From c15d970826e73bcd8f81e6202696aeebbce8053e Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Thu, 23 Oct 2025 00:02:10 -0500 Subject: [PATCH 1/3] Setup linkry lambda @ edge --- src/linkryEdgeFunction/main.py | 112 +++++++++++++++++++++++++ terraform/modules/lambdas/main.tf | 68 +++++++++++++++ terraform/modules/lambdas/variables.tf | 6 ++ 3 files changed, 186 insertions(+) create mode 100644 src/linkryEdgeFunction/main.py diff --git a/src/linkryEdgeFunction/main.py b/src/linkryEdgeFunction/main.py new file mode 100644 index 00000000..d76f9067 --- /dev/null +++ b/src/linkryEdgeFunction/main.py @@ -0,0 +1,112 @@ +import json +import os +import boto3 +from botocore.exceptions import ClientError + +DEFAULT_AWS_REGION = "us-east-2" +AVAILABLE_REPLICAS = os.environ.get("DYNAMODB_REPLICAS", DEFAULT_AWS_REGION).split(",") +DYNAMODB_TABLE = "infra-core-api-linkry" +FALLBACK_URL = os.environ.get("FALLBACK_URL", "https://acm.illinois.edu/404") +LINKRY_HOME_URL = os.environ.get( + "LINKRY_HOME_URL", "https://core.acm.illinois.edu/linkry" +) +CACHE_TTL = "30" # seconds to hold response in PoP + + +def select_replica(lambda_region): + """Determine which DynamoDB replica to use based on Lambda execution region""" + # First check if Lambda is running in a replica region + if lambda_region in AVAILABLE_REPLICAS: + return lambda_region + + # Otherwise, find nearest replica by region prefix matching + region_prefix = "-".join(lambda_region.split("-")[:2]) + + for replica in AVAILABLE_REPLICAS: + if replica.startswith(region_prefix): + return replica + + return DEFAULT_AWS_REGION + + +current_region = os.environ.get("AWS_REGION", "us-east-2") +target_region = select_replica(current_region) +dynamodb = boto3.client("dynamodb", region_name=target_region) + +print(f"Lambda in {current_region}, routing DynamoDB to {target_region}") + + +def handler(event, context): + request = event["Records"][0]["cf"]["request"] + path = request["uri"].lstrip("/") + + print(f"Processing path: {path}") + + if not path: + return { + "status": "301", + "statusDescription": "Moved Permanently", + "headers": { + "location": [{"key": "Location", "value": LINKRY_HOME_URL}], + "cache-control": [ + {"key": "Cache-Control", "value": f"public, max-age={CACHE_TTL}"} + ], + }, + } + + # Query DynamoDB for records with PK=path and SK starting with "OWNER#" + try: + response = dynamodb.query( + TableName=DYNAMODB_TABLE, + KeyConditionExpression="slug = :slug AND begins_with(access, :owner_prefix)", + ExpressionAttributeValues={ + ":slug": {"S": path}, + ":owner_prefix": {"S": "OWNER#"}, + }, + Limit=1, # We only need one result + ) + + print(f"DynamoDB query response: {json.dumps(response, default=str)}") + + if response.get("Items") and len(response["Items"]) > 0: + item = response["Items"][0] + + # Extract the redirect URL from the item + redirect_url = item.get("redirect", {}).get("S") + + if redirect_url: + print(f"Found redirect: {path} -> {redirect_url}") + return { + "status": "302", + "statusDescription": "Found", + "headers": { + "location": [{"key": "Location", "value": redirect_url}], + "cache-control": [ + { + "key": "Cache-Control", + "value": f"public, max-age={CACHE_TTL}", + } + ], + }, + } + else: + print(f"Item found but no redirect attribute for path: {path}") + else: + print(f"No items found for path: {path}") + + except ClientError as e: + print(f"DynamoDB query failed for {path} in region {target_region}: {e}") + except Exception as e: + print(f"Unexpected error: {e}") + + # Not found - redirect to fallback + return { + "status": "307", + "statusDescription": "Temporary Redirect", + "headers": { + "location": [{"key": "Location", "value": FALLBACK_URL}], + "cache-control": [ + {"key": "Cache-Control", "value": f"public, max-age={CACHE_TTL}"} + ], + }, + } diff --git a/terraform/modules/lambdas/main.tf b/terraform/modules/lambdas/main.tf index 01bd4729..a3b65d26 100644 --- a/terraform/modules/lambdas/main.tf +++ b/terraform/modules/lambdas/main.tf @@ -10,6 +10,12 @@ data "archive_file" "sqs_lambda_code" { output_path = "${path.module}/../../../dist/terraform/sqs.zip" } +data "archive_file" "linkry_edge_lambda_code" { + type = "zip" + source_dir = "${path.module}/../../../src/linkryEdgeFunction/" + output_path = "${path.module}/../../../dist/terraform/linkryEdgeFunction.zip" +} + locals { core_api_lambda_name = "${var.ProjectId}-main-server" core_api_slow_lambda_name = "${var.ProjectId}-slow-server" @@ -444,7 +450,69 @@ module "lambda_warmer_slow" { is_streaming_lambda = true } +// Linkry Lambda @ Edge +resource "aws_iam_role" "linkry_lambda_edge_role" { + name = "${var.ProjectId}-linkry-edge-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ] + } + Action = "sts:AssumeRole" + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "linkry_lambda_edge_basic" { + role = aws_iam_role.linkry_lambda_edge_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +} + +resource "aws_iam_role_policy" "linkry_lambda_edge_dynamodb" { + name = "${var.ProjectId}-linkry-edge-dynamodb" + role = aws_iam_role.linkry_lambda_edge_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "dynamodb:GetItem", + "dynamodb:Query" + ] + Resource = [ + "arn:aws:dynamodb:*:${data.aws_caller_identity.current.account_id}:table/${var.ProjectId}-linkry" + ] + } + ] + }) +} + +resource "aws_lambda_function" "linkry_edge" { + region = "us-east-1" + filename = data.archive_file.linkry_edge_lambda_code.output_path + function_name = "${var.ProjectId}-linkry-edge" + role = aws_iam_role.linkry_lambda_edge_role.arn + handler = "main.handler" + runtime = "python3.12" # Changed to Python runtime + publish = true + timeout = 5 + source_code_hash = data.archive_file.linkry_edge_lambda_code.output_base64sha256 + environment { + variables = { + DYNAMODB_REPLICAS = join(",", var.LinkryReplicationRegions) + } + } +} // Outputs output "core_function_url" { diff --git a/terraform/modules/lambdas/variables.tf b/terraform/modules/lambdas/variables.tf index ae119bf6..e4f611a1 100644 --- a/terraform/modules/lambdas/variables.tf +++ b/terraform/modules/lambdas/variables.tf @@ -37,3 +37,9 @@ variable "LogRetentionDays" { variable "EmailDomain" { type = string } + +variable "LinkryReplicationRegions" { + type = set(string) + description = "A list of regions where the Linkry data has be replicated to (in addition to the primary region)" +} + From 1d9ecdbf0ec6908433a6e7790f9fa5157cc6048a Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Thu, 23 Oct 2025 01:24:08 -0500 Subject: [PATCH 2/3] Linkry: read on redirect from DynamoDB instead of cloudfront KV store --- src/api/functions/cloudfrontKvStore.ts | 152 ------------------------ src/api/package.json | 1 - src/api/routes/linkry.ts | 57 +-------- src/api/types.d.ts | 2 - src/common/config.ts | 2 - src/linkryEdgeFunction/main.py | 9 +- terraform/envs/prod/main.tf | 31 +++-- terraform/envs/qa/main.tf | 32 +++-- terraform/modules/archival/main.tf | 21 +--- terraform/modules/archival/variables.tf | 4 + terraform/modules/dynamo/main.tf | 2 + terraform/modules/frontend/main.tf | 53 +-------- terraform/modules/frontend/variables.tf | 2 +- terraform/modules/lambdas/main.tf | 23 +--- terraform/modules/lambdas/variables.tf | 5 - tests/unit/linkry.test.ts | 15 --- yarn.lock | 46 ------- 17 files changed, 55 insertions(+), 402 deletions(-) delete mode 100644 src/api/functions/cloudfrontKvStore.ts diff --git a/src/api/functions/cloudfrontKvStore.ts b/src/api/functions/cloudfrontKvStore.ts deleted file mode 100644 index 3fc09e22..00000000 --- a/src/api/functions/cloudfrontKvStore.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { - CloudFrontKeyValueStoreClient, - ConflictException, - DeleteKeyCommand, - DescribeKeyValueStoreCommand, - GetKeyCommand, - PutKeyCommand, -} from "@aws-sdk/client-cloudfront-keyvaluestore"; -import { environmentConfig } from "common/config.js"; -import { - DatabaseDeleteError, - DatabaseFetchError, - DatabaseInsertError, - InternalServerError, -} from "common/errors/index.js"; -import { RunEnvironment } from "common/roles.js"; -import "@aws-sdk/signature-v4-crt"; - -const INITIAL_CONFLICT_WAIT_PERIOD = 150; -const CONFLICT_NUM_RETRIES = 3; - -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -export const setKey = async ({ - key, - value, - arn, - kvsClient, -}: { - key: string; - value: string; - arn: string; - kvsClient: CloudFrontKeyValueStoreClient; -}) => { - let numRetries = 0; - let currentWaitPeriod = INITIAL_CONFLICT_WAIT_PERIOD; - while (numRetries < CONFLICT_NUM_RETRIES) { - const command = new DescribeKeyValueStoreCommand({ KvsARN: arn }); - const response = await kvsClient.send(command); - const etag = response.ETag; - const putCommand = new PutKeyCommand({ - IfMatch: etag, - Key: key, - Value: value, - KvsARN: arn, - }); - try { - await kvsClient.send(putCommand); - return; - } catch (e) { - if (e instanceof ConflictException) { - numRetries++; - await sleep(currentWaitPeriod); - currentWaitPeriod *= 2; - continue; - } else { - throw e; - } - } - } - throw new DatabaseInsertError({ - message: "Failed to save redirect to Cloudfront KV store.", - }); -}; - -export const deleteKey = async ({ - key, - arn, - kvsClient, -}: { - key: string; - arn: string; - kvsClient: CloudFrontKeyValueStoreClient; -}) => { - let numRetries = 0; - let currentWaitPeriod = INITIAL_CONFLICT_WAIT_PERIOD; - while (numRetries < CONFLICT_NUM_RETRIES) { - const command = new DescribeKeyValueStoreCommand({ KvsARN: arn }); - const response = await kvsClient.send(command); - const etag = response.ETag; - const putCommand = new DeleteKeyCommand({ - IfMatch: etag, - Key: key, - KvsARN: arn, - }); - try { - await kvsClient.send(putCommand); - return; - } catch (e) { - if (e instanceof ConflictException) { - numRetries++; - await sleep(currentWaitPeriod); - currentWaitPeriod *= 2; - continue; - } else { - throw e; - } - } - } - throw new DatabaseDeleteError({ - message: "Failed to save delete to Cloudfront KV store.", - }); -}; - -export const getKey = async ({ - key, - arn, - kvsClient, -}: { - key: string; - arn: string; - kvsClient: CloudFrontKeyValueStoreClient; -}) => { - let numRetries = 0; - let currentWaitPeriod = INITIAL_CONFLICT_WAIT_PERIOD; - while (numRetries < CONFLICT_NUM_RETRIES) { - const getCommand = new GetKeyCommand({ - Key: key, - KvsARN: arn, - }); - try { - const response = await kvsClient.send(getCommand); - return response.Value; - } catch (e) { - if (e instanceof ConflictException) { - numRetries++; - await sleep(currentWaitPeriod); - currentWaitPeriod *= 2; - continue; - } else { - throw e; - } - } - } - throw new DatabaseFetchError({ - message: "Failed to retrieve value from Cloudfront KV store.", - }); -}; - -export const getLinkryKvArn = async (runEnvironment: RunEnvironment) => { - if (process.env.LinkryKvArn) { - return process.env.LinkryKvArn; - } - if (environmentConfig[runEnvironment].LinkryCloudfrontKvArn) { - return environmentConfig[runEnvironment].LinkryCloudfrontKvArn; - } - throw new InternalServerError({ - message: "Could not find the Cloudfront Key-Value store ARN", - }); -}; diff --git a/src/api/package.json b/src/api/package.json index 6328edac..fca3bb45 100644 --- a/src/api/package.json +++ b/src/api/package.json @@ -15,7 +15,6 @@ "prettier:write": "prettier --write *.ts **/*.ts" }, "dependencies": { - "@aws-sdk/client-cloudfront-keyvaluestore": "^3.895.0", "@aws-sdk/client-dynamodb": "^3.895.0", "@aws-sdk/client-lambda": "^3.895.0", "@aws-sdk/client-secrets-manager": "^3.895.0", diff --git a/src/api/routes/linkry.ts b/src/api/routes/linkry.ts index 32327219..cd1829f6 100644 --- a/src/api/routes/linkry.ts +++ b/src/api/routes/linkry.ts @@ -10,22 +10,15 @@ import { UnauthorizedError, ValidationError, } from "../../common/errors/index.js"; -import { NoDataRequest } from "../types.js"; import { QueryCommand, TransactWriteItemsCommand, TransactWriteItem, TransactionCanceledException, } from "@aws-sdk/client-dynamodb"; -import { CloudFrontKeyValueStoreClient } from "@aws-sdk/client-cloudfront-keyvaluestore"; import { genericConfig } from "../../common/config.js"; import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; import rateLimiter from "api/plugins/rateLimiter.js"; -import { - deleteKey, - getLinkryKvArn, - setKey, -} from "api/functions/cloudfrontKvStore.js"; import { createRequest, linkrySlug } from "common/types/linkry.js"; import { extractUniqueSlugs, @@ -191,12 +184,6 @@ const linkryRoutes: FastifyPluginAsync = async (fastify, _options) => { message: `Slug ${request.body.slug} is reserved by the system.`, }); } - - if (!fastify.cloudfrontKvClient) { - fastify.cloudfrontKvClient = new CloudFrontKeyValueStoreClient({ - region: genericConfig.AwsRegion, - }); - } }, onRequest: fastify.authorizeFromSchema, }, @@ -423,24 +410,6 @@ const linkryRoutes: FastifyPluginAsync = async (fastify, _options) => { message: "Failed to save data to DynamoDB.", }); } - // Add to cloudfront key value store so that redirects happen at the edge - const kvArn = await getLinkryKvArn(fastify.runEnvironment); - try { - await setKey({ - key: request.body.slug, - value: request.body.redirect, - kvsClient: fastify.cloudfrontKvClient, - arn: kvArn, - }); - } catch (e) { - fastify.log.error(e); - if (e instanceof BaseError) { - throw e; - } - throw new DatabaseInsertError({ - message: "Failed to save redirect to Cloudfront KV store.", - }); - } await createAuditLogEntry({ dynamoClient: fastify.dynamoClient, entry: { @@ -517,15 +486,7 @@ const linkryRoutes: FastifyPluginAsync = async (fastify, _options) => { }), }), ), - onRequest: async (request, reply) => { - await fastify.authorizeFromSchema(request, reply); - - if (!fastify.cloudfrontKvClient) { - fastify.cloudfrontKvClient = new CloudFrontKeyValueStoreClient({ - region: genericConfig.AwsRegion, - }); - } - }, + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { const { slug } = request.params; @@ -614,22 +575,6 @@ const linkryRoutes: FastifyPluginAsync = async (fastify, _options) => { message: "Failed to delete data from DynamoDB.", }); } - const kvArn = await getLinkryKvArn(fastify.runEnvironment); - try { - await deleteKey({ - key: slug, - kvsClient: fastify.cloudfrontKvClient, - arn: kvArn, - }); - } catch (e) { - fastify.log.error(e); - if (e instanceof BaseError) { - throw e; - } - throw new DatabaseDeleteError({ - message: "Failed to delete redirect at Cloudfront KV store.", - }); - } await createAuditLogEntry({ dynamoClient: fastify.dynamoClient, entry: { diff --git a/src/api/types.d.ts b/src/api/types.d.ts index f50b9c48..8213f193 100644 --- a/src/api/types.d.ts +++ b/src/api/types.d.ts @@ -7,7 +7,6 @@ import NodeCache from "node-cache"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import { SQSClient } from "@aws-sdk/client-sqs"; -import { CloudFrontKeyValueStoreClient } from "@aws-sdk/client-cloudfront-keyvaluestore"; import { AvailableAuthorizationPolicy } from "common/policies/definition.js"; import type RedisModule from "ioredis"; export type Redis = RedisModule.default; @@ -45,7 +44,6 @@ declare module "fastify" { sqsClient?: SQSClient; redisClient: Redis; secretsManagerClient: SecretsManagerClient; - cloudfrontKvClient: CloudFrontKeyValueStoreClient; secretConfig: SecretConfig | (SecretConfig & SecretTesting); refreshSecretConfig: CallableFunction; } diff --git a/src/common/config.ts b/src/common/config.ts index fc1d6ca8..618f8fe6 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -25,7 +25,6 @@ export type ConfigType = { PaidMemberGroupId: string; PaidMemberPriceId: string; AadValidReadOnlyClientId: string; - LinkryCloudfrontKvArn?: string; ConfigurationSecretIds: string[]; DiscordGuildId: string; GroupSuffix: string; @@ -136,7 +135,6 @@ const environmentConfig: EnvironmentConfigType = { PaidMemberGroupId: "9222451f-b354-4e64-ba28-c0f367a277c2", PaidMemberPriceId: "price_1S5eAqDGHrJxx3mKZYGoulj3", AadValidReadOnlyClientId: "2c6a0057-5acc-496c-a4e5-4adbf88387ba", - LinkryCloudfrontKvArn: "arn:aws:cloudfront::427040638965:key-value-store/0c2c02fd-7c47-4029-975d-bc5d0376bba1", DiscordGuildId: "1278798685706391664", EntraServicePrincipalId: "8c26ff11-fb86-42f2-858b-9011c9f0708d", GroupSuffix: "[NonProd]", diff --git a/src/linkryEdgeFunction/main.py b/src/linkryEdgeFunction/main.py index d76f9067..70db59f8 100644 --- a/src/linkryEdgeFunction/main.py +++ b/src/linkryEdgeFunction/main.py @@ -4,7 +4,9 @@ from botocore.exceptions import ClientError DEFAULT_AWS_REGION = "us-east-2" -AVAILABLE_REPLICAS = os.environ.get("DYNAMODB_REPLICAS", DEFAULT_AWS_REGION).split(",") +AVAILABLE_REPLICAS = [ + "us-west-2", +] DYNAMODB_TABLE = "infra-core-api-linkry" FALLBACK_URL = os.environ.get("FALLBACK_URL", "https://acm.illinois.edu/404") LINKRY_HOME_URL = os.environ.get( @@ -21,6 +23,8 @@ def select_replica(lambda_region): # Otherwise, find nearest replica by region prefix matching region_prefix = "-".join(lambda_region.split("-")[:2]) + if region_prefix == "us": + return DEFAULT_AWS_REGION for replica in AVAILABLE_REPLICAS: if replica.startswith(region_prefix): @@ -63,11 +67,10 @@ def handler(event, context): ":slug": {"S": path}, ":owner_prefix": {"S": "OWNER#"}, }, + ProjectionExpression="redirect", Limit=1, # We only need one result ) - print(f"DynamoDB query response: {json.dumps(response, default=str)}") - if response.get("Items") and len(response["Items"]) > 0: item = response["Items"][0] diff --git a/terraform/envs/prod/main.tf b/terraform/envs/prod/main.tf index e8fe592c..4292e302 100644 --- a/terraform/envs/prod/main.tf +++ b/terraform/envs/prod/main.tf @@ -37,7 +37,7 @@ locals { main = module.sqs_queues.main_queue_arn sqs = module.sqs_queues.sales_email_queue_arn } - LinkryReplicationRegions = toset(["us-east-1", "us-west-2", "eu-central-1", "ap-south-1"]) + LinkryReplicationRegions = toset(["us-west-2"]) } module "sqs_queues" { @@ -57,10 +57,6 @@ module "origin_verify" { ProjectId = var.ProjectId } -resource "aws_cloudfront_key_value_store" "linkry_kv" { - name = "${var.ProjectId}-cloudfront-linkry-kv" -} - module "alarms" { source = "../../modules/alarms" priority_sns_arn = var.PrioritySNSAlertArn @@ -82,6 +78,7 @@ module "archival" { source = "../../modules/archival" ProjectId = var.ProjectId RunEnvironment = "dev" + BucketPrefix = local.bucket_prefix LogRetentionDays = var.LogRetentionDays MonitorTables = ["${var.ProjectId}-audit-log", "${var.ProjectId}-events", "${var.ProjectId}-room-requests"] TableDeletionDays = tomap({ @@ -95,26 +92,26 @@ module "lambdas" { source = "../../modules/lambdas" ProjectId = var.ProjectId RunEnvironment = "prod" - LinkryKvArn = aws_cloudfront_key_value_store.linkry_kv.arn CurrentOriginVerifyKey = module.origin_verify.current_origin_verify_key PreviousOriginVerifyKey = module.origin_verify.previous_origin_verify_key PreviousOriginVerifyKeyExpiresAt = module.origin_verify.previous_invalid_time LogRetentionDays = var.LogRetentionDays EmailDomain = var.EmailDomain + LinkryReplicationRegions = local.LinkryReplicationRegions } module "frontend" { - source = "../../modules/frontend" - BucketPrefix = local.bucket_prefix - CoreLambdaHost = module.lambdas.core_function_url - OriginVerifyKey = module.origin_verify.current_origin_verify_key - ProjectId = var.ProjectId - CoreCertificateArn = var.CoreCertificateArn - CorePublicDomain = var.CorePublicDomain - CoreSlowLambdaHost = module.lambdas.core_slow_function_url - IcalPublicDomain = var.IcalPublicDomain - LinkryPublicDomain = var.LinkryPublicDomain - LinkryKvArn = aws_cloudfront_key_value_store.linkry_kv.arn + source = "../../modules/frontend" + BucketPrefix = local.bucket_prefix + CoreLambdaHost = module.lambdas.core_function_url + OriginVerifyKey = module.origin_verify.current_origin_verify_key + ProjectId = var.ProjectId + CoreCertificateArn = var.CoreCertificateArn + CorePublicDomain = var.CorePublicDomain + CoreSlowLambdaHost = module.lambdas.core_slow_function_url + IcalPublicDomain = var.IcalPublicDomain + LinkryPublicDomain = var.LinkryPublicDomain + LinkryEdgeFunctionArn = module.lambdas.linkry_redirect_function_arn } module "assets" { diff --git a/terraform/envs/qa/main.tf b/terraform/envs/qa/main.tf index 2321e4d4..3d34294b 100644 --- a/terraform/envs/qa/main.tf +++ b/terraform/envs/qa/main.tf @@ -32,7 +32,7 @@ data "aws_caller_identity" "current" {} data "aws_region" "current" {} locals { - LinkryReplicationRegions = toset(["us-east-1", "us-west-2", "eu-central-1", "ap-south-1"]) + LinkryReplicationRegions = toset(["us-west-2"]) } @@ -84,6 +84,7 @@ module "archival" { RunEnvironment = "dev" LogRetentionDays = var.LogRetentionDays MonitorTables = ["${var.ProjectId}-audit-log", "${var.ProjectId}-events", "${var.ProjectId}-room-requests"] + BucketPrefix = local.bucket_prefix TableDeletionDays = tomap({ "${var.ProjectId}-audit-log" : 15, "${var.ProjectId}-room-requests" : 15 @@ -92,35 +93,30 @@ module "archival" { }) } -resource "aws_cloudfront_key_value_store" "linkry_kv" { - name = "${var.ProjectId}-cloudfront-linkry-kv" -} - - module "lambdas" { source = "../../modules/lambdas" ProjectId = var.ProjectId RunEnvironment = "dev" - LinkryKvArn = aws_cloudfront_key_value_store.linkry_kv.arn CurrentOriginVerifyKey = module.origin_verify.current_origin_verify_key PreviousOriginVerifyKey = module.origin_verify.previous_origin_verify_key PreviousOriginVerifyKeyExpiresAt = module.origin_verify.previous_invalid_time LogRetentionDays = var.LogRetentionDays EmailDomain = var.EmailDomain + LinkryReplicationRegions = local.LinkryReplicationRegions } module "frontend" { - source = "../../modules/frontend" - BucketPrefix = local.bucket_prefix - CoreLambdaHost = module.lambdas.core_function_url - CoreSlowLambdaHost = module.lambdas.core_slow_function_url - OriginVerifyKey = module.origin_verify.current_origin_verify_key - ProjectId = var.ProjectId - CoreCertificateArn = var.CoreCertificateArn - CorePublicDomain = var.CorePublicDomain - IcalPublicDomain = var.IcalPublicDomain - LinkryPublicDomain = var.LinkryPublicDomain - LinkryKvArn = aws_cloudfront_key_value_store.linkry_kv.arn + source = "../../modules/frontend" + BucketPrefix = local.bucket_prefix + CoreLambdaHost = module.lambdas.core_function_url + CoreSlowLambdaHost = module.lambdas.core_slow_function_url + OriginVerifyKey = module.origin_verify.current_origin_verify_key + ProjectId = var.ProjectId + CoreCertificateArn = var.CoreCertificateArn + CorePublicDomain = var.CorePublicDomain + IcalPublicDomain = var.IcalPublicDomain + LinkryPublicDomain = var.LinkryPublicDomain + LinkryEdgeFunctionArn = module.lambdas.linkry_redirect_function_arn } module "assets" { diff --git a/terraform/modules/archival/main.tf b/terraform/modules/archival/main.tf index 3d969e60..8d19248c 100644 --- a/terraform/modules/archival/main.tf +++ b/terraform/modules/archival/main.tf @@ -5,41 +5,34 @@ data "archive_file" "api_lambda_code" { } locals { - aws_region = "us-east-2" - dynamo_stream_reader_lambda_name = "${var.ProjectId}-${local.aws_region}-dynamo-archival" - firehose_stream_name = "${var.ProjectId}-${local.aws_region}-archival-stream" - bucket_prefix = "${data.aws_caller_identity.current.account_id}-${local.aws_region}" + dynamo_stream_reader_lambda_name = "${var.ProjectId}-dynamo-archival" + firehose_stream_name = "${var.ProjectId}-archival-stream" } data "aws_caller_identity" "current" {} data "aws_region" "current" {} resource "aws_cloudwatch_log_group" "archive_logs" { - region = local.aws_region name = "/aws/lambda/${local.dynamo_stream_reader_lambda_name}" retention_in_days = var.LogRetentionDays } resource "aws_cloudwatch_log_group" "firehose_logs" { - region = local.aws_region name = "/aws/kinesisfirehose/${local.firehose_stream_name}" retention_in_days = var.LogRetentionDays } resource "aws_cloudwatch_log_stream" "firehose_logs_stream" { - region = local.aws_region log_group_name = aws_cloudwatch_log_group.firehose_logs.name name = "DataArchivalS3Delivery" } resource "aws_s3_bucket" "this" { - region = local.aws_region - bucket = "${local.bucket_prefix}-ddb-archive" + bucket = "${var.BucketPrefix}-ddb-archive" } resource "aws_s3_bucket_versioning" "this" { - region = local.aws_region bucket = aws_s3_bucket.this.id versioning_configuration { status = "Enabled" @@ -47,7 +40,6 @@ resource "aws_s3_bucket_versioning" "this" { } resource "aws_s3_bucket_lifecycle_configuration" "this" { - region = local.aws_region bucket = aws_s3_bucket.this.id rule { @@ -101,7 +93,6 @@ resource "aws_s3_bucket_lifecycle_configuration" "this" { } resource "aws_s3_bucket_intelligent_tiering_configuration" "this" { - region = local.aws_region bucket = aws_s3_bucket.this.id name = "ArchiveAfterSixMonths" status = "Enabled" @@ -144,13 +135,11 @@ resource "aws_iam_policy" "archive_lambda_policy" { data "aws_dynamodb_table" "existing_tables" { - region = local.aws_region for_each = toset(var.MonitorTables) name = each.key } resource "aws_lambda_event_source_mapping" "stream_mapping" { - region = local.aws_region for_each = toset(var.MonitorTables) function_name = aws_lambda_function.api_lambda.arn event_source_arn = data.aws_dynamodb_table.existing_tables[each.key].stream_arn @@ -218,7 +207,7 @@ resource "aws_iam_policy" "firehose_policy" { "logs:CreateLogStream", "logs:PutLogEvents" ] - Resource = ["arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/kinesisfirehose/${local.firehose_stream_name}:*"] + Resource = ["arn:aws:logs:*:${data.aws_caller_identity.current.account_id}:log-group:/aws/kinesisfirehose/${local.firehose_stream_name}:*"] } ] }) @@ -230,7 +219,6 @@ resource "aws_iam_role_policy_attachment" "firehose_attach" { } resource "aws_kinesis_firehose_delivery_stream" "dynamic_stream" { - region = local.aws_region name = local.firehose_stream_name destination = "extended_s3" @@ -312,7 +300,6 @@ resource "aws_iam_role_policy_attachment" "archive_attach" { } resource "aws_lambda_function" "api_lambda" { - region = local.aws_region depends_on = [aws_cloudwatch_log_group.archive_logs] function_name = local.dynamo_stream_reader_lambda_name role = aws_iam_role.archive_role.arn diff --git a/terraform/modules/archival/variables.tf b/terraform/modules/archival/variables.tf index 0f11799b..9e332052 100644 --- a/terraform/modules/archival/variables.tf +++ b/terraform/modules/archival/variables.tf @@ -3,6 +3,10 @@ variable "ProjectId" { description = "Prefix before each resource" } +variable "BucketPrefix" { + type = string +} + variable "LogRetentionDays" { type = number } diff --git a/terraform/modules/dynamo/main.tf b/terraform/modules/dynamo/main.tf index d8488850..ff575e14 100644 --- a/terraform/modules/dynamo/main.tf +++ b/terraform/modules/dynamo/main.tf @@ -303,6 +303,8 @@ resource "aws_dynamodb_table" "linkry_records" { range_key = "slug" projection_type = "ALL" } + stream_enabled = true + stream_view_type = "NEW_AND_OLD_IMAGES" } resource "aws_dynamodb_table" "cache" { diff --git a/terraform/modules/frontend/main.tf b/terraform/modules/frontend/main.tf index e1e6b5a6..2cf1359b 100644 --- a/terraform/modules/frontend/main.tf +++ b/terraform/modules/frontend/main.tf @@ -314,52 +314,6 @@ function handler(event) { EOT } -resource "aws_cloudfront_function" "linkry_redirect" { - name = "${var.ProjectId}-linkry-edge-redir" - comment = "Linkry Redirect @ Edge" - key_value_store_associations = [var.LinkryKvArn] - runtime = "cloudfront-js-2.0" - code = < { - return { - setKey: vi.fn(), - deleteKey: vi.fn(), - getKey: vi.fn().mockResolvedValue("https://www.acm.illinois.edu"), - getLinkryKvArn: vi - .fn() - .mockResolvedValue( - "arn:aws:cloudfront::1234567890:key-value-store/bb90421c-e923-4bd7-a42a-7281150389c3s", - ), - }; -}); - const app = await init(); (app as any).nodeCache.flushAll(); diff --git a/yarn.lock b/yarn.lock index 7cd3370b..fb508773 100644 --- a/yarn.lock +++ b/yarn.lock @@ -78,52 +78,6 @@ "@smithy/util-utf8" "^2.0.0" tslib "^2.6.2" -"@aws-sdk/client-cloudfront-keyvaluestore@^3.895.0": - version "3.896.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudfront-keyvaluestore/-/client-cloudfront-keyvaluestore-3.896.0.tgz#2064bc02a5b47dcfd07c2a7b254ef4dfcea8bf34" - integrity sha512-lBeLongETFtoiZKTSHvItBTxyAHfAM9kQXcEapKWnI+XB1lp4xLw7+v1ymIkDYRgXD5bBAx3DAxLHmxeR9fNZg== - dependencies: - "@aws-crypto/sha256-browser" "5.2.0" - "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "3.896.0" - "@aws-sdk/credential-provider-node" "3.896.0" - "@aws-sdk/middleware-host-header" "3.893.0" - "@aws-sdk/middleware-logger" "3.893.0" - "@aws-sdk/middleware-recursion-detection" "3.893.0" - "@aws-sdk/middleware-user-agent" "3.896.0" - "@aws-sdk/region-config-resolver" "3.893.0" - "@aws-sdk/signature-v4-multi-region" "3.896.0" - "@aws-sdk/types" "3.893.0" - "@aws-sdk/util-endpoints" "3.895.0" - "@aws-sdk/util-user-agent-browser" "3.893.0" - "@aws-sdk/util-user-agent-node" "3.896.0" - "@smithy/config-resolver" "^4.2.2" - "@smithy/core" "^3.12.0" - "@smithy/fetch-http-handler" "^5.2.1" - "@smithy/hash-node" "^4.1.1" - "@smithy/invalid-dependency" "^4.1.1" - "@smithy/middleware-content-length" "^4.1.1" - "@smithy/middleware-endpoint" "^4.2.4" - "@smithy/middleware-retry" "^4.3.0" - "@smithy/middleware-serde" "^4.1.1" - "@smithy/middleware-stack" "^4.1.1" - "@smithy/node-config-provider" "^4.2.2" - "@smithy/node-http-handler" "^4.2.1" - "@smithy/protocol-http" "^5.2.1" - "@smithy/smithy-client" "^4.6.4" - "@smithy/types" "^4.5.0" - "@smithy/url-parser" "^4.1.1" - "@smithy/util-base64" "^4.1.0" - "@smithy/util-body-length-browser" "^4.1.0" - "@smithy/util-body-length-node" "^4.1.0" - "@smithy/util-defaults-mode-browser" "^4.1.4" - "@smithy/util-defaults-mode-node" "^4.1.4" - "@smithy/util-endpoints" "^3.1.2" - "@smithy/util-middleware" "^4.1.1" - "@smithy/util-retry" "^4.1.2" - "@smithy/util-utf8" "^4.1.0" - tslib "^2.6.2" - "@aws-sdk/client-dynamodb@^3.895.0": version "3.896.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-dynamodb/-/client-dynamodb-3.896.0.tgz#a01a32af6a72890cba6952e01ca8b6289c85090c" From f323330e85b2b856af36662179258c7f2e0f62df Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Thu, 23 Oct 2025 01:31:40 -0500 Subject: [PATCH 3/3] Remove type --- src/ui/types.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui/types.d.ts b/src/ui/types.d.ts index e0330c11..14c95662 100644 --- a/src/ui/types.d.ts +++ b/src/ui/types.d.ts @@ -9,7 +9,6 @@ import type NodeCache from "node-cache"; import { type DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { type SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import { type SQSClient } from "@aws-sdk/client-sqs"; -import { type CloudFrontKeyValueStoreClient } from "@aws-sdk/client-cloudfront-keyvaluestore"; import { type AvailableAuthorizationPolicy } from "@common/policies/definition.js"; declare module "fastify" {