From 1e45134b263bf0674a9c21d0518f012a7b8a4682 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Mon, 5 Jan 2026 19:09:01 +0530 Subject: [PATCH 01/25] Add conditional creation to NAT and VPC endpoints --- infrastructure/modules/networking/main.tf | 14 ++++++++++---- infrastructure/modules/networking/outputs.tf | 4 ++-- infrastructure/modules/networking/variables.tf | 12 ++++++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/infrastructure/modules/networking/main.tf b/infrastructure/modules/networking/main.tf index 88e1169fbb..9643e2a3a8 100644 --- a/infrastructure/modules/networking/main.tf +++ b/infrastructure/modules/networking/main.tf @@ -43,6 +43,7 @@ module "nacl" { } module "vpc_endpoint" { + count = var.create_vpc_endpoints ? 1 : 0 source = "./modules/vpc-endpoint" aws_region = var.aws_region @@ -129,6 +130,7 @@ resource "aws_subnet" "private" { } resource "aws_eip" "nat" { + count = var.create_nat_gateway ? 1 : 0 depends_on = [aws_internet_gateway.main] domain = "vpc" tags = merge(var.common_tags, { @@ -137,7 +139,8 @@ resource "aws_eip" "nat" { } resource "aws_nat_gateway" "main" { - allocation_id = aws_eip.nat.id + count = var.create_nat_gateway ? 1 : 0 + allocation_id = aws_eip.nat[0].id depends_on = [aws_internet_gateway.main] subnet_id = aws_subnet.public[0].id tags = merge(var.common_tags, { @@ -157,9 +160,12 @@ resource "aws_route_table" "public" { } resource "aws_route_table" "private" { - route { - cidr_block = "0.0.0.0/0" - nat_gateway_id = aws_nat_gateway.main.id + dynamic "route" { + for_each = var.create_nat_gateway ? [1] : [] + content { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.main[0].id + } } tags = merge(var.common_tags, { Name = "${var.project_name}-${var.environment}-private-rt" diff --git a/infrastructure/modules/networking/outputs.tf b/infrastructure/modules/networking/outputs.tf index 30d22bef66..d93b912fa7 100644 --- a/infrastructure/modules/networking/outputs.tf +++ b/infrastructure/modules/networking/outputs.tf @@ -14,6 +14,6 @@ output "private_subnet_ids" { } output "vpc_endpoint_security_group_id" { - description = "Security group ID for VPC endpoints." - value = module.vpc_endpoint.security_group_id + description = "Security group ID for VPC endpoints (null if disabled)." + value = var.create_vpc_endpoints ? module.vpc_endpoint[0].security_group_id : null } diff --git a/infrastructure/modules/networking/variables.tf b/infrastructure/modules/networking/variables.tf index 30c74cb251..9e103c4b22 100644 --- a/infrastructure/modules/networking/variables.tf +++ b/infrastructure/modules/networking/variables.tf @@ -14,6 +14,18 @@ variable "common_tags" { default = {} } +variable "create_nat_gateway" { + description = "Whether to create a NAT Gateway for private subnet internet access." + type = bool + default = true +} + +variable "create_vpc_endpoints" { + description = "Whether to create VPC Endpoints for AWS services." + type = bool + default = true +} + variable "environment" { description = "The environment (e.g., staging, production)." type = string From 99e39ebdca26e350fc891bd24e2de34be9741549 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Mon, 5 Jan 2026 19:24:26 +0530 Subject: [PATCH 02/25] Add conditional FARGATE SPOT to ECS --- infrastructure/modules/ecs/main.tf | 39 +++++++++++++++---- .../modules/ecs/modules/task/main.tf | 13 +++++-- .../modules/ecs/modules/task/variables.tf | 16 +++++++- infrastructure/modules/ecs/variables.tf | 16 +++++++- infrastructure/staging/main.tf | 2 +- 5 files changed, 70 insertions(+), 16 deletions(-) diff --git a/infrastructure/modules/ecs/main.tf b/infrastructure/modules/ecs/main.tf index a46b0cac75..99cc2d049a 100644 --- a/infrastructure/modules/ecs/main.tf +++ b/infrastructure/modules/ecs/main.tf @@ -16,6 +16,17 @@ resource "aws_ecs_cluster" "main" { tags = var.common_tags } +resource "aws_ecs_cluster_capacity_providers" "main" { + capacity_providers = ["FARGATE", "FARGATE_SPOT"] + cluster_name = aws_ecs_cluster.main.name + + default_capacity_provider_strategy { + base = 0 + capacity_provider = var.use_fargate_spot ? "FARGATE_SPOT" : "FARGATE" + weight = 1 + } +} + resource "aws_ecr_lifecycle_policy" "main" { repository = aws_ecr_repository.main.name @@ -208,6 +219,7 @@ resource "aws_iam_role_policy_attachment" "event_bridge_policy_attachment" { module "sync_data_task" { source = "./modules/task" + assign_public_ip = var.assign_public_ip aws_region = var.aws_region command = ["/bin/sh", "-c", "EXEC_MODE=direct make sync-data"] common_tags = var.common_tags @@ -219,17 +231,19 @@ module "sync_data_task" { event_bridge_role_arn = aws_iam_role.event_bridge_role.arn image_url = "${aws_ecr_repository.main.repository_url}:${var.image_tag}" memory = var.sync_data_task_memory - private_subnet_ids = var.private_subnet_ids project_name = var.project_name schedule_expression = "cron(17 05 * * ? *)" security_group_ids = [var.ecs_sg_id] + subnet_ids = var.subnet_ids task_name = "sync-data" + use_fargate_spot = var.use_fargate_spot } module "owasp_update_project_health_metrics_task" { source = "./modules/task" - aws_region = var.aws_region + assign_public_ip = var.assign_public_ip + aws_region = var.aws_region command = [ "/bin/sh", "-c", @@ -248,16 +262,18 @@ module "owasp_update_project_health_metrics_task" { event_bridge_role_arn = aws_iam_role.event_bridge_role.arn image_url = "${aws_ecr_repository.main.repository_url}:${var.image_tag}" memory = var.update_project_health_metrics_task_memory - private_subnet_ids = var.private_subnet_ids project_name = var.project_name schedule_expression = "cron(17 17 * * ? *)" security_group_ids = [var.ecs_sg_id] + subnet_ids = var.subnet_ids task_name = "owasp-update-project-health-metrics" + use_fargate_spot = var.use_fargate_spot } module "owasp_update_project_health_scores_task" { source = "./modules/task" + assign_public_ip = var.assign_public_ip aws_region = var.aws_region command = ["/bin/sh", "-c", "EXEC_MODE=direct make owasp-update-project-health-scores"] common_tags = var.common_tags @@ -269,16 +285,18 @@ module "owasp_update_project_health_scores_task" { event_bridge_role_arn = aws_iam_role.event_bridge_role.arn image_url = "${aws_ecr_repository.main.repository_url}:${var.image_tag}" memory = var.update_project_health_scores_task_memory - private_subnet_ids = var.private_subnet_ids project_name = var.project_name schedule_expression = "cron(22 17 * * ? *)" security_group_ids = [var.ecs_sg_id] + subnet_ids = var.subnet_ids task_name = "owasp-update-project-health-scores" + use_fargate_spot = var.use_fargate_spot } module "migrate_task" { source = "./modules/task" + assign_public_ip = var.assign_public_ip aws_region = var.aws_region command = ["/bin/sh", "-c", "EXEC_MODE=direct make migrate"] common_tags = var.common_tags @@ -289,16 +307,18 @@ module "migrate_task" { environment = var.environment image_url = "${aws_ecr_repository.main.repository_url}:${var.image_tag}" memory = var.migrate_task_memory - private_subnet_ids = var.private_subnet_ids project_name = var.project_name security_group_ids = [var.ecs_sg_id] + subnet_ids = var.subnet_ids task_name = "migrate" + use_fargate_spot = var.use_fargate_spot } module "load_data_task" { source = "./modules/task" - aws_region = var.aws_region + assign_public_ip = var.assign_public_ip + aws_region = var.aws_region command = [ "/bin/sh", "-c", @@ -320,16 +340,18 @@ module "load_data_task" { environment = var.environment image_url = "${aws_ecr_repository.main.repository_url}:${var.image_tag}" memory = var.load_data_task_memory - private_subnet_ids = var.private_subnet_ids project_name = var.project_name security_group_ids = [var.ecs_sg_id] + subnet_ids = var.subnet_ids task_name = "load-data" task_role_arn = aws_iam_role.ecs_task_role.arn + use_fargate_spot = var.use_fargate_spot } module "index_data_task" { source = "./modules/task" + assign_public_ip = var.assign_public_ip aws_region = var.aws_region command = ["/bin/sh", "-c", "EXEC_MODE=direct make index-data"] common_tags = var.common_tags @@ -340,8 +362,9 @@ module "index_data_task" { environment = var.environment image_url = "${aws_ecr_repository.main.repository_url}:${var.image_tag}" memory = var.index_data_task_memory - private_subnet_ids = var.private_subnet_ids project_name = var.project_name security_group_ids = [var.ecs_sg_id] + subnet_ids = var.subnet_ids task_name = "index-data" + use_fargate_spot = var.use_fargate_spot } diff --git a/infrastructure/modules/ecs/modules/task/main.tf b/infrastructure/modules/ecs/modules/task/main.tf index 6f71792290..c66628822f 100644 --- a/infrastructure/modules/ecs/modules/task/main.tf +++ b/infrastructure/modules/ecs/modules/task/main.tf @@ -72,11 +72,18 @@ resource "aws_cloudwatch_event_target" "task" { ecs_target { task_definition_arn = aws_ecs_task_definition.task.arn - launch_type = "FARGATE" + launch_type = null + + capacity_provider_strategy { + capacity_provider = var.use_fargate_spot ? "FARGATE_SPOT" : "FARGATE" + weight = 1 + base = 0 + } + network_configuration { - subnets = var.private_subnet_ids + subnets = var.subnet_ids security_groups = var.security_group_ids - assign_public_ip = false + assign_public_ip = var.assign_public_ip } } } diff --git a/infrastructure/modules/ecs/modules/task/variables.tf b/infrastructure/modules/ecs/modules/task/variables.tf index 0853df8847..ab0aae3505 100644 --- a/infrastructure/modules/ecs/modules/task/variables.tf +++ b/infrastructure/modules/ecs/modules/task/variables.tf @@ -1,3 +1,9 @@ +variable "assign_public_ip" { + description = "Whether to assign public IPs to ECS tasks." + type = bool + default = false +} + variable "aws_region" { description = "The AWS region for the CloudWatch logs." type = string @@ -62,8 +68,8 @@ variable "memory" { type = string } -variable "private_subnet_ids" { - description = "A list of private subnet IDs for the task." +variable "subnet_ids" { + description = "Subnet IDs for the task (can be public or private)." type = list(string) } @@ -93,3 +99,9 @@ variable "task_role_arn" { type = string default = null } + +variable "use_fargate_spot" { + description = "Whether to use Fargate Spot capacity provider." + type = bool + default = false +} diff --git a/infrastructure/modules/ecs/variables.tf b/infrastructure/modules/ecs/variables.tf index 12530eb1b8..d83160ccef 100644 --- a/infrastructure/modules/ecs/variables.tf +++ b/infrastructure/modules/ecs/variables.tf @@ -1,3 +1,9 @@ +variable "assign_public_ip" { + description = "Whether to assign public IPs to ECS tasks (required for public subnets)." + type = bool + default = false +} + variable "aws_region" { description = "The AWS region." type = string @@ -77,8 +83,8 @@ variable "migrate_task_memory" { default = "1024" } -variable "private_subnet_ids" { - description = "A list of private subnet IDs." +variable "subnet_ids" { + description = "Subnet IDs for ECS tasks (can be public or private)." type = list(string) } @@ -99,6 +105,12 @@ variable "sync_data_task_memory" { default = "1024" } +variable "use_fargate_spot" { + description = "Whether to use Fargate Spot capacity provider for cost savings." + type = bool + default = false +} + variable "update_project_health_metrics_task_cpu" { description = "The CPU for the update-project-health-metrics task." type = string diff --git a/infrastructure/staging/main.tf b/infrastructure/staging/main.tf index 242b2d8717..96325378c0 100644 --- a/infrastructure/staging/main.tf +++ b/infrastructure/staging/main.tf @@ -64,7 +64,7 @@ module "ecs" { environment = var.environment fixtures_read_only_policy_arn = module.storage.fixtures_read_only_policy_arn fixtures_bucket_name = module.storage.fixtures_s3_bucket_name - private_subnet_ids = module.networking.private_subnet_ids + subnet_ids = module.networking.private_subnet_ids project_name = var.project_name } From 4e172428bb4b416a53304c98c94a638993a7160f Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Mon, 5 Jan 2026 19:26:43 +0530 Subject: [PATCH 03/25] Update security module --- infrastructure/modules/security/main.tf | 2 ++ infrastructure/modules/security/variables.tf | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/infrastructure/modules/security/main.tf b/infrastructure/modules/security/main.tf index 3ac8fc1cf4..e6db877763 100644 --- a/infrastructure/modules/security/main.tf +++ b/infrastructure/modules/security/main.tf @@ -114,6 +114,7 @@ resource "aws_security_group_rule" "ecs_egress_all" { } resource "aws_security_group_rule" "ecs_to_vpc_endpoints" { + count = var.vpc_endpoint_sg_id != null ? 1 : 0 description = "Allow HTTPS to VPC endpoints" from_port = 443 protocol = "tcp" @@ -164,6 +165,7 @@ resource "aws_security_group_rule" "lambda_egress_all" { } resource "aws_security_group_rule" "lambda_to_vpc_endpoints" { + count = var.vpc_endpoint_sg_id != null ? 1 : 0 description = "Allow HTTPS to VPC endpoints" from_port = 443 protocol = "tcp" diff --git a/infrastructure/modules/security/variables.tf b/infrastructure/modules/security/variables.tf index 725b7aa4f4..e1466bb451 100644 --- a/infrastructure/modules/security/variables.tf +++ b/infrastructure/modules/security/variables.tf @@ -37,8 +37,9 @@ variable "redis_port" { } variable "vpc_endpoint_sg_id" { - description = "Security group ID for VPC endpoints." + description = "Security group ID for VPC endpoints (null if VPC endpoints disabled)." type = string + default = null } variable "vpc_id" { From bd7d4e8d7d9b2b407c3fdae7b445878cad39b26f Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Mon, 5 Jan 2026 20:03:05 +0530 Subject: [PATCH 04/25] Add conditional FARGATE SPOT to frontend --- infrastructure/modules/frontend/main.tf | 7 ++++++- infrastructure/modules/frontend/variables.tf | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/infrastructure/modules/frontend/main.tf b/infrastructure/modules/frontend/main.tf index 057e6f0c37..b921a54247 100644 --- a/infrastructure/modules/frontend/main.tf +++ b/infrastructure/modules/frontend/main.tf @@ -124,13 +124,18 @@ resource "aws_ecs_service" "frontend" { cluster = aws_ecs_cluster.frontend.id desired_count = var.desired_count health_check_grace_period_seconds = 60 - launch_type = "FARGATE" name = "${var.project_name}-${var.environment}-frontend-service" tags = merge(var.common_tags, { Name = "${var.project_name}-${var.environment}-frontend-service" }) task_definition = aws_ecs_task_definition.frontend.arn + capacity_provider_strategy { + base = 0 + capacity_provider = var.use_fargate_spot ? "FARGATE_SPOT" : "FARGATE" + weight = 1 + } + load_balancer { container_name = "frontend" container_port = 3000 diff --git a/infrastructure/modules/frontend/variables.tf b/infrastructure/modules/frontend/variables.tf index 7564ed47b5..dc3115f342 100644 --- a/infrastructure/modules/frontend/variables.tf +++ b/infrastructure/modules/frontend/variables.tf @@ -110,6 +110,12 @@ variable "public_subnet_ids" { type = list(string) } +variable "use_fargate_spot" { + description = "Use Fargate Spot capacity provider for frontend tasks." + type = bool + default = false +} + variable "vpc_id" { description = "The VPC ID." type = string From da3e80fa30e1d8698acee73bd7bb7bf7079c109e Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Mon, 5 Jan 2026 20:19:22 +0530 Subject: [PATCH 05/25] Update staging configuration --- infrastructure/staging/main.tf | 9 ++++-- .../staging/terraform.tfvars.example | 10 +++--- infrastructure/staging/variables.tf | 32 ++++++++++++++++++- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/infrastructure/staging/main.tf b/infrastructure/staging/main.tf index 96325378c0..e7b545509b 100644 --- a/infrastructure/staging/main.tf +++ b/infrastructure/staging/main.tf @@ -57,15 +57,17 @@ module "database" { module "ecs" { source = "../modules/ecs" + assign_public_ip = var.ecs_use_public_subnets aws_region = var.aws_region common_tags = local.common_tags container_parameters_arns = module.parameters.django_ssm_parameter_arns ecs_sg_id = module.security.ecs_sg_id environment = var.environment - fixtures_read_only_policy_arn = module.storage.fixtures_read_only_policy_arn fixtures_bucket_name = module.storage.fixtures_s3_bucket_name - subnet_ids = module.networking.private_subnet_ids + fixtures_read_only_policy_arn = module.storage.fixtures_read_only_policy_arn project_name = var.project_name + subnet_ids = var.ecs_use_public_subnets ? module.networking.public_subnet_ids : module.networking.private_subnet_ids + use_fargate_spot = var.use_fargate_spot } module "frontend" { @@ -86,6 +88,7 @@ module "frontend" { private_subnet_ids = module.networking.private_subnet_ids project_name = var.project_name public_subnet_ids = module.networking.public_subnet_ids + use_fargate_spot = var.frontend_use_fargate_spot vpc_id = module.networking.vpc_id } @@ -95,6 +98,8 @@ module "networking" { aws_region = var.aws_region availability_zones = var.availability_zones common_tags = local.common_tags + create_nat_gateway = var.create_nat_gateway + create_vpc_endpoints = var.create_vpc_endpoints environment = var.environment private_subnet_cidrs = var.private_subnet_cidrs project_name = var.project_name diff --git a/infrastructure/staging/terraform.tfvars.example b/infrastructure/staging/terraform.tfvars.example index 8e4aa5c576..26e511d07e 100644 --- a/infrastructure/staging/terraform.tfvars.example +++ b/infrastructure/staging/terraform.tfvars.example @@ -1,8 +1,10 @@ availability_zones = ["us-east-2a", "us-east-2b", "us-east-2c"] aws_region = "us-east-2" -create_rds_proxy = true -db_name = "owasp_nest" -db_user = "owasp_nest_db_user" -db_port = 5432 +create_nat_gateway = false +create_rds_proxy = false +create_vpc_endpoints = false +ecs_use_public_subnets = true environment = "staging" +frontend_use_fargate_spot = true project_name = "owasp-nest" +use_fargate_spot = true diff --git a/infrastructure/staging/variables.tf b/infrastructure/staging/variables.tf index 290e088613..b16afec06d 100644 --- a/infrastructure/staging/variables.tf +++ b/infrastructure/staging/variables.tf @@ -10,10 +10,22 @@ variable "availability_zones" { default = ["us-east-2a", "us-east-2b", "us-east-2c"] } +variable "create_nat_gateway" { + description = "Whether to create a NAT Gateway for private subnet internet access." + type = bool + default = false +} + variable "create_rds_proxy" { description = "Whether to create an RDS proxy." type = bool - default = true + default = false +} + +variable "create_vpc_endpoints" { + description = "Whether to create VPC Endpoints for AWS services." + type = bool + default = false } variable "db_allocated_storage" { @@ -103,6 +115,12 @@ variable "environment" { } } +variable "ecs_use_public_subnets" { + description = "Whether to run ECS tasks in public subnets (requires assign_public_ip)." + type = bool + default = true +} + variable "fixtures_bucket_name" { description = "The name of the S3 bucket for fixtures." type = string @@ -145,6 +163,12 @@ variable "frontend_min_count" { default = 2 } +variable "frontend_use_fargate_spot" { + description = "Whether to use Fargate Spot for frontend tasks." + type = bool + default = true +} + variable "private_subnet_cidrs" { description = "A list of CIDR blocks for the private subnets." type = list(string) @@ -217,6 +241,12 @@ variable "secret_recovery_window_in_days" { default = 7 } +variable "use_fargate_spot" { + description = "Whether to use Fargate Spot for backend ECS tasks." + type = bool + default = true +} + variable "vpc_cidr" { description = "The CIDR block for the VPC." type = string From a9b10c0ae5857189e48c682b8521ccf9bc24a561 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Tue, 6 Jan 2026 17:12:21 +0530 Subject: [PATCH 06/25] Simplify frontend deployment steps --- infrastructure/modules/parameters/main.tf | 10 +++++----- infrastructure/modules/parameters/variables.tf | 13 +++++++++++-- infrastructure/modules/security/main.tf | 1 + infrastructure/staging/main.tf | 5 ++++- infrastructure/staging/outputs.tf | 2 +- infrastructure/staging/terraform.tfvars.example | 1 + infrastructure/staging/variables.tf | 8 +------- 7 files changed, 24 insertions(+), 16 deletions(-) diff --git a/infrastructure/modules/parameters/main.tf b/infrastructure/modules/parameters/main.tf index a8f17093fe..11299fa15a 100644 --- a/infrastructure/modules/parameters/main.tf +++ b/infrastructure/modules/parameters/main.tf @@ -38,7 +38,7 @@ resource "aws_ssm_parameter" "django_algolia_write_api_key" { } resource "aws_ssm_parameter" "django_allowed_hosts" { - description = "The allowed hosts." + description = "Django allowed hosts - hostname only, no protocol (e.g., nest.owasp.dev)." name = "/${var.project_name}/${var.environment}/DJANGO_ALLOWED_HOSTS" tags = var.common_tags type = "String" @@ -50,11 +50,11 @@ resource "aws_ssm_parameter" "django_allowed_hosts" { } resource "aws_ssm_parameter" "django_allowed_origins" { - description = "A Comma-separated list of allowed CORS origins for Django." + description = "Django allowed CORS origins - full URL with protocol (e.g., https://nest.owasp.dev)." name = "/${var.project_name}/${var.environment}/DJANGO_ALLOWED_ORIGINS" tags = var.common_tags type = "String" - value = "to-be-set-in-aws-console" + value = var.allowed_origins lifecycle { ignore_changes = [value] @@ -286,11 +286,11 @@ resource "aws_ssm_parameter" "nextauth_secret" { } resource "aws_ssm_parameter" "nextauth_url" { - description = "NextAuth URL (frontend base URL)." + description = "NextAuth base URL - full URL with protocol (e.g., https://nest.owasp.dev)." name = "/${var.project_name}/${var.environment}/NEXTAUTH_URL" tags = var.common_tags type = "String" - value = "to-be-set-in-aws-console" + value = var.nextauth_url lifecycle { ignore_changes = [value] diff --git a/infrastructure/modules/parameters/variables.tf b/infrastructure/modules/parameters/variables.tf index 6d76403d11..da0c444f8e 100644 --- a/infrastructure/modules/parameters/variables.tf +++ b/infrastructure/modules/parameters/variables.tf @@ -1,7 +1,11 @@ variable "allowed_hosts" { - description = "The Django allowed hosts." + description = "The Django allowed hosts - hostname only, no protocol (e.g., nest.owasp.dev)." + type = string +} + +variable "allowed_origins" { + description = "The Django allowed CORS origins (comma-separated URLs with protocol)." type = string - default = "*" } variable "common_tags" { @@ -47,6 +51,11 @@ variable "environment" { type = string } +variable "nextauth_url" { + description = "The NextAuth base URL (frontend URL with protocol)." + type = string +} + variable "project_name" { description = "The name of the project." type = string diff --git a/infrastructure/modules/security/main.tf b/infrastructure/modules/security/main.tf index e6db877763..e9f44ffac1 100644 --- a/infrastructure/modules/security/main.tf +++ b/infrastructure/modules/security/main.tf @@ -145,6 +145,7 @@ resource "aws_security_group_rule" "frontend_https" { } resource "aws_security_group_rule" "frontend_to_vpc_endpoints" { + count = var.vpc_endpoint_sg_id != null ? 1 : 0 description = "Allow HTTPS to VPC endpoints" from_port = 443 protocol = "tcp" diff --git a/infrastructure/staging/main.tf b/infrastructure/staging/main.tf index e7b545509b..57132afddf 100644 --- a/infrastructure/staging/main.tf +++ b/infrastructure/staging/main.tf @@ -79,7 +79,7 @@ module "frontend" { desired_count = var.frontend_desired_count domain_name = var.frontend_domain_name enable_auto_scaling = var.frontend_enable_auto_scaling - enable_https = var.frontend_enable_https + enable_https = var.frontend_domain_name != null environment = var.environment frontend_parameters_arns = module.parameters.frontend_ssm_parameter_arns frontend_sg_id = module.security.frontend_sg_id @@ -110,6 +110,8 @@ module "networking" { module "parameters" { source = "../modules/parameters" + allowed_hosts = var.frontend_domain_name != null ? var.frontend_domain_name : module.frontend.alb_dns_name + allowed_origins = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.frontend.alb_dns_name}" common_tags = local.common_tags db_host = module.database.db_proxy_endpoint db_name = var.db_name @@ -117,6 +119,7 @@ module "parameters" { db_port = var.db_port db_user = var.db_user environment = var.environment + nextauth_url = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.frontend.alb_dns_name}" project_name = var.project_name redis_host = module.cache.redis_primary_endpoint redis_password_arn = module.cache.redis_password_arn diff --git a/infrastructure/staging/outputs.tf b/infrastructure/staging/outputs.tf index 12f4af7844..d58c64ef15 100644 --- a/infrastructure/staging/outputs.tf +++ b/infrastructure/staging/outputs.tf @@ -25,7 +25,7 @@ output "frontend_ecr_repository_url" { output "frontend_url" { description = "The URL to access the frontend." - value = var.frontend_enable_https && var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.frontend.alb_dns_name}" + value = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.frontend.alb_dns_name}" } output "private_subnet_ids" { diff --git a/infrastructure/staging/terraform.tfvars.example b/infrastructure/staging/terraform.tfvars.example index 26e511d07e..7c99d21948 100644 --- a/infrastructure/staging/terraform.tfvars.example +++ b/infrastructure/staging/terraform.tfvars.example @@ -5,6 +5,7 @@ create_rds_proxy = false create_vpc_endpoints = false ecs_use_public_subnets = true environment = "staging" +frontend_domain_name = https://nest.owasp.dev frontend_use_fargate_spot = true project_name = "owasp-nest" use_fargate_spot = true diff --git a/infrastructure/staging/variables.tf b/infrastructure/staging/variables.tf index b16afec06d..17b8f96237 100644 --- a/infrastructure/staging/variables.tf +++ b/infrastructure/staging/variables.tf @@ -134,7 +134,7 @@ variable "frontend_desired_count" { } variable "frontend_domain_name" { - description = "The domain name for frontend (required for HTTPS)" + description = "The domain name for frontend. When set, HTTPS is auto-enabled via ACM." type = string default = null } @@ -145,12 +145,6 @@ variable "frontend_enable_auto_scaling" { default = false } -variable "frontend_enable_https" { - description = "Whether to enable HTTPS listener on ALB." - type = bool - default = false -} - variable "frontend_max_count" { description = "The maximum number of tasks for auto scaling." type = number From 6da11c0291344267fc3d05d1ce09298e9d0be131 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Tue, 6 Jan 2026 19:24:53 +0530 Subject: [PATCH 07/25] Create VPC endpoints conditionally --- infrastructure/modules/networking/main.tf | 8 +++- .../networking/modules/vpc-endpoint/main.tf | 31 +++++++++---- .../modules/vpc-endpoint/outputs.tf | 2 +- .../modules/vpc-endpoint/variables.tf | 36 ++++++++++++++++ infrastructure/modules/networking/outputs.tf | 2 +- .../modules/networking/variables.tf | 36 ++++++++++++++-- infrastructure/modules/parameters/main.tf | 2 +- .../modules/parameters/variables.tf | 3 +- infrastructure/modules/security/main.tf | 6 +-- infrastructure/modules/security/variables.tf | 6 +++ infrastructure/staging/main.tf | 43 +++++++++++-------- .../staging/terraform.tfvars.example | 21 +++++---- infrastructure/staging/variables.tf | 36 ++++++++++++++-- 13 files changed, 180 insertions(+), 52 deletions(-) diff --git a/infrastructure/modules/networking/main.tf b/infrastructure/modules/networking/main.tf index 9643e2a3a8..ad9bb08e36 100644 --- a/infrastructure/modules/networking/main.tf +++ b/infrastructure/modules/networking/main.tf @@ -43,11 +43,17 @@ module "nacl" { } module "vpc_endpoint" { - count = var.create_vpc_endpoints ? 1 : 0 + count = (var.create_vpc_cloudwatch_logs_endpoint || var.create_vpc_ecr_api_endpoint || var.create_vpc_ecr_dkr_endpoint || var.create_vpc_s3_endpoint || var.create_vpc_secretsmanager_endpoint || var.create_vpc_ssm_endpoint) ? 1 : 0 source = "./modules/vpc-endpoint" aws_region = var.aws_region common_tags = var.common_tags + create_cloudwatch_logs = var.create_vpc_cloudwatch_logs_endpoint + create_ecr_api = var.create_vpc_ecr_api_endpoint + create_ecr_dkr = var.create_vpc_ecr_dkr_endpoint + create_s3 = var.create_vpc_s3_endpoint + create_secretsmanager = var.create_vpc_secretsmanager_endpoint + create_ssm = var.create_vpc_ssm_endpoint environment = var.environment private_route_table_id = aws_route_table.private.id private_subnet_ids = aws_subnet.private[*].id diff --git a/infrastructure/modules/networking/modules/vpc-endpoint/main.tf b/infrastructure/modules/networking/modules/vpc-endpoint/main.tf index 0cf8bfcd82..bee698da64 100644 --- a/infrastructure/modules/networking/modules/vpc-endpoint/main.tf +++ b/infrastructure/modules/networking/modules/vpc-endpoint/main.tf @@ -9,7 +9,13 @@ terraform { } } +locals { + # Check if any interface endpoints are enabled (need security group) + any_interface_endpoint = var.create_cloudwatch_logs || var.create_ecr_api || var.create_ecr_dkr || var.create_secretsmanager || var.create_ssm +} + resource "aws_security_group" "vpc_endpoints" { + count = local.any_interface_endpoint ? 1 : 0 description = "Security group for VPC endpoints" name = "${var.project_name}-${var.environment}-vpc-endpoints-sg" tags = merge(var.common_tags, { @@ -19,18 +25,20 @@ resource "aws_security_group" "vpc_endpoints" { } resource "aws_security_group_rule" "vpc_endpoints_ingress_https" { + count = local.any_interface_endpoint ? 1 : 0 cidr_blocks = [var.vpc_cidr] description = "Allow HTTPS from VPC" from_port = 443 protocol = "tcp" - security_group_id = aws_security_group.vpc_endpoints.id + security_group_id = aws_security_group.vpc_endpoints[0].id to_port = 443 type = "ingress" } resource "aws_vpc_endpoint" "cloudwatch_logs" { + count = var.create_cloudwatch_logs ? 1 : 0 private_dns_enabled = true - security_group_ids = [aws_security_group.vpc_endpoints.id] + security_group_ids = [aws_security_group.vpc_endpoints[0].id] service_name = "com.amazonaws.${var.aws_region}.logs" subnet_ids = var.private_subnet_ids tags = merge(var.common_tags, { @@ -41,8 +49,9 @@ resource "aws_vpc_endpoint" "cloudwatch_logs" { } resource "aws_vpc_endpoint" "ecr_api" { + count = var.create_ecr_api ? 1 : 0 private_dns_enabled = true - security_group_ids = [aws_security_group.vpc_endpoints.id] + security_group_ids = [aws_security_group.vpc_endpoints[0].id] service_name = "com.amazonaws.${var.aws_region}.ecr.api" subnet_ids = var.private_subnet_ids tags = merge(var.common_tags, { @@ -53,8 +62,9 @@ resource "aws_vpc_endpoint" "ecr_api" { } resource "aws_vpc_endpoint" "ecr_dkr" { + count = var.create_ecr_dkr ? 1 : 0 private_dns_enabled = true - security_group_ids = [aws_security_group.vpc_endpoints.id] + security_group_ids = [aws_security_group.vpc_endpoints[0].id] service_name = "com.amazonaws.${var.aws_region}.ecr.dkr" subnet_ids = var.private_subnet_ids tags = merge(var.common_tags, { @@ -65,6 +75,7 @@ resource "aws_vpc_endpoint" "ecr_dkr" { } resource "aws_vpc_endpoint" "s3" { + count = var.create_s3 ? 1 : 0 service_name = "com.amazonaws.${var.aws_region}.s3" tags = merge(var.common_tags, { Name = "${var.project_name}-${var.environment}-s3-endpoint" @@ -74,8 +85,9 @@ resource "aws_vpc_endpoint" "s3" { } resource "aws_vpc_endpoint" "secretsmanager" { + count = var.create_secretsmanager ? 1 : 0 private_dns_enabled = true - security_group_ids = [aws_security_group.vpc_endpoints.id] + security_group_ids = [aws_security_group.vpc_endpoints[0].id] service_name = "com.amazonaws.${var.aws_region}.secretsmanager" subnet_ids = var.private_subnet_ids tags = merge(var.common_tags, { @@ -86,8 +98,9 @@ resource "aws_vpc_endpoint" "secretsmanager" { } resource "aws_vpc_endpoint" "ssm" { + count = var.create_ssm ? 1 : 0 private_dns_enabled = true - security_group_ids = [aws_security_group.vpc_endpoints.id] + security_group_ids = [aws_security_group.vpc_endpoints[0].id] service_name = "com.amazonaws.${var.aws_region}.ssm" subnet_ids = var.private_subnet_ids tags = merge(var.common_tags, { @@ -98,11 +111,13 @@ resource "aws_vpc_endpoint" "ssm" { } resource "aws_vpc_endpoint_route_table_association" "s3_private" { + count = var.create_s3 ? 1 : 0 route_table_id = var.private_route_table_id - vpc_endpoint_id = aws_vpc_endpoint.s3.id + vpc_endpoint_id = aws_vpc_endpoint.s3[0].id } resource "aws_vpc_endpoint_route_table_association" "s3_public" { + count = var.create_s3 ? 1 : 0 route_table_id = var.public_route_table_id - vpc_endpoint_id = aws_vpc_endpoint.s3.id + vpc_endpoint_id = aws_vpc_endpoint.s3[0].id } diff --git a/infrastructure/modules/networking/modules/vpc-endpoint/outputs.tf b/infrastructure/modules/networking/modules/vpc-endpoint/outputs.tf index fc9d266fc5..134ed2cb1c 100644 --- a/infrastructure/modules/networking/modules/vpc-endpoint/outputs.tf +++ b/infrastructure/modules/networking/modules/vpc-endpoint/outputs.tf @@ -1,4 +1,4 @@ output "security_group_id" { description = "Security group ID for VPC endpoints." - value = aws_security_group.vpc_endpoints.id + value = length(aws_security_group.vpc_endpoints) > 0 ? aws_security_group.vpc_endpoints[0].id : null } diff --git a/infrastructure/modules/networking/modules/vpc-endpoint/variables.tf b/infrastructure/modules/networking/modules/vpc-endpoint/variables.tf index 3c567b18c0..98ed879f47 100644 --- a/infrastructure/modules/networking/modules/vpc-endpoint/variables.tf +++ b/infrastructure/modules/networking/modules/vpc-endpoint/variables.tf @@ -8,6 +8,42 @@ variable "common_tags" { type = map(string) } +variable "create_cloudwatch_logs" { + description = "Whether to create CloudWatch Logs VPC endpoint." + type = bool + default = true +} + +variable "create_ecr_api" { + description = "Whether to create ECR API VPC endpoint." + type = bool + default = true +} + +variable "create_ecr_dkr" { + description = "Whether to create ECR DKR VPC endpoint." + type = bool + default = true +} + +variable "create_s3" { + description = "Whether to create S3 VPC endpoint (Gateway, free)." + type = bool + default = true +} + +variable "create_secretsmanager" { + description = "Whether to create Secrets Manager VPC endpoint." + type = bool + default = true +} + +variable "create_ssm" { + description = "Whether to create SSM VPC endpoint." + type = bool + default = true +} + variable "environment" { description = "The environment (e.g., staging, production)." type = string diff --git a/infrastructure/modules/networking/outputs.tf b/infrastructure/modules/networking/outputs.tf index d93b912fa7..14c690a832 100644 --- a/infrastructure/modules/networking/outputs.tf +++ b/infrastructure/modules/networking/outputs.tf @@ -15,5 +15,5 @@ output "private_subnet_ids" { output "vpc_endpoint_security_group_id" { description = "Security group ID for VPC endpoints (null if disabled)." - value = var.create_vpc_endpoints ? module.vpc_endpoint[0].security_group_id : null + value = length(module.vpc_endpoint) > 0 ? module.vpc_endpoint[0].security_group_id : null } diff --git a/infrastructure/modules/networking/variables.tf b/infrastructure/modules/networking/variables.tf index 9e103c4b22..c5417dff81 100644 --- a/infrastructure/modules/networking/variables.tf +++ b/infrastructure/modules/networking/variables.tf @@ -20,10 +20,40 @@ variable "create_nat_gateway" { default = true } -variable "create_vpc_endpoints" { - description = "Whether to create VPC Endpoints for AWS services." +variable "create_vpc_cloudwatch_logs_endpoint" { + description = "Whether to create CloudWatch Logs VPC endpoint." type = bool - default = true + default = false +} + +variable "create_vpc_ecr_api_endpoint" { + description = "Whether to create ECR API VPC endpoint." + type = bool + default = false +} + +variable "create_vpc_ecr_dkr_endpoint" { + description = "Whether to create ECR DKR VPC endpoint." + type = bool + default = false +} + +variable "create_vpc_s3_endpoint" { + description = "Whether to create S3 VPC endpoint (Gateway, free)." + type = bool + default = false +} + +variable "create_vpc_secretsmanager_endpoint" { + description = "Whether to create Secrets Manager VPC endpoint." + type = bool + default = false +} + +variable "create_vpc_ssm_endpoint" { + description = "Whether to create SSM VPC endpoint." + type = bool + default = false } variable "environment" { diff --git a/infrastructure/modules/parameters/main.tf b/infrastructure/modules/parameters/main.tf index 11299fa15a..7befbf7bd4 100644 --- a/infrastructure/modules/parameters/main.tf +++ b/infrastructure/modules/parameters/main.tf @@ -38,7 +38,7 @@ resource "aws_ssm_parameter" "django_algolia_write_api_key" { } resource "aws_ssm_parameter" "django_allowed_hosts" { - description = "Django allowed hosts - hostname only, no protocol (e.g., nest.owasp.dev)." + description = "Django allowed hosts for API Gateway - hostname only, no protocol (e.g., xxx.execute-api.region.amazonaws.com)." name = "/${var.project_name}/${var.environment}/DJANGO_ALLOWED_HOSTS" tags = var.common_tags type = "String" diff --git a/infrastructure/modules/parameters/variables.tf b/infrastructure/modules/parameters/variables.tf index da0c444f8e..ff5ca8e743 100644 --- a/infrastructure/modules/parameters/variables.tf +++ b/infrastructure/modules/parameters/variables.tf @@ -1,6 +1,7 @@ variable "allowed_hosts" { - description = "The Django allowed hosts - hostname only, no protocol (e.g., nest.owasp.dev)." + description = "Django allowed hosts for API Gateway - hostname only, no protocol (e.g., xxx.execute-api.region.amazonaws.com)." type = string + default = "*" } variable "allowed_origins" { diff --git a/infrastructure/modules/security/main.tf b/infrastructure/modules/security/main.tf index e9f44ffac1..acc1fa8864 100644 --- a/infrastructure/modules/security/main.tf +++ b/infrastructure/modules/security/main.tf @@ -114,7 +114,7 @@ resource "aws_security_group_rule" "ecs_egress_all" { } resource "aws_security_group_rule" "ecs_to_vpc_endpoints" { - count = var.vpc_endpoint_sg_id != null ? 1 : 0 + count = var.create_vpc_endpoint_rules ? 1 : 0 description = "Allow HTTPS to VPC endpoints" from_port = 443 protocol = "tcp" @@ -145,7 +145,7 @@ resource "aws_security_group_rule" "frontend_https" { } resource "aws_security_group_rule" "frontend_to_vpc_endpoints" { - count = var.vpc_endpoint_sg_id != null ? 1 : 0 + count = var.create_vpc_endpoint_rules ? 1 : 0 description = "Allow HTTPS to VPC endpoints" from_port = 443 protocol = "tcp" @@ -166,7 +166,7 @@ resource "aws_security_group_rule" "lambda_egress_all" { } resource "aws_security_group_rule" "lambda_to_vpc_endpoints" { - count = var.vpc_endpoint_sg_id != null ? 1 : 0 + count = var.create_vpc_endpoint_rules ? 1 : 0 description = "Allow HTTPS to VPC endpoints" from_port = 443 protocol = "tcp" diff --git a/infrastructure/modules/security/variables.tf b/infrastructure/modules/security/variables.tf index e1466bb451..0fd1ff7572 100644 --- a/infrastructure/modules/security/variables.tf +++ b/infrastructure/modules/security/variables.tf @@ -10,6 +10,12 @@ variable "create_rds_proxy" { default = false } +variable "create_vpc_endpoint_rules" { + description = "Whether to create security group rules for VPC endpoints." + type = bool + default = false +} + variable "db_port" { description = "The port for the RDS database." type = number diff --git a/infrastructure/staging/main.tf b/infrastructure/staging/main.tf index 57132afddf..339d9e1e9f 100644 --- a/infrastructure/staging/main.tf +++ b/infrastructure/staging/main.tf @@ -95,22 +95,26 @@ module "frontend" { module "networking" { source = "../modules/networking" - aws_region = var.aws_region - availability_zones = var.availability_zones - common_tags = local.common_tags - create_nat_gateway = var.create_nat_gateway - create_vpc_endpoints = var.create_vpc_endpoints - environment = var.environment - private_subnet_cidrs = var.private_subnet_cidrs - project_name = var.project_name - public_subnet_cidrs = var.public_subnet_cidrs - vpc_cidr = var.vpc_cidr + aws_region = var.aws_region + availability_zones = var.availability_zones + common_tags = local.common_tags + create_nat_gateway = var.create_nat_gateway + create_vpc_cloudwatch_logs_endpoint = var.create_vpc_cloudwatch_logs_endpoint + create_vpc_ecr_api_endpoint = var.create_vpc_ecr_api_endpoint + create_vpc_ecr_dkr_endpoint = var.create_vpc_ecr_dkr_endpoint + create_vpc_s3_endpoint = var.create_vpc_s3_endpoint + create_vpc_secretsmanager_endpoint = var.create_vpc_secretsmanager_endpoint + create_vpc_ssm_endpoint = var.create_vpc_ssm_endpoint + environment = var.environment + private_subnet_cidrs = var.private_subnet_cidrs + project_name = var.project_name + public_subnet_cidrs = var.public_subnet_cidrs + vpc_cidr = var.vpc_cidr } module "parameters" { source = "../modules/parameters" - allowed_hosts = var.frontend_domain_name != null ? var.frontend_domain_name : module.frontend.alb_dns_name allowed_origins = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.frontend.alb_dns_name}" common_tags = local.common_tags db_host = module.database.db_proxy_endpoint @@ -128,14 +132,15 @@ module "parameters" { module "security" { source = "../modules/security" - common_tags = local.common_tags - create_rds_proxy = var.create_rds_proxy - db_port = var.db_port - environment = var.environment - project_name = var.project_name - redis_port = var.redis_port - vpc_endpoint_sg_id = module.networking.vpc_endpoint_security_group_id - vpc_id = module.networking.vpc_id + common_tags = local.common_tags + create_rds_proxy = var.create_rds_proxy + create_vpc_endpoint_rules = var.create_vpc_ssm_endpoint || var.create_vpc_cloudwatch_logs_endpoint || var.create_vpc_ecr_api_endpoint || var.create_vpc_ecr_dkr_endpoint || var.create_vpc_secretsmanager_endpoint + db_port = var.db_port + environment = var.environment + project_name = var.project_name + redis_port = var.redis_port + vpc_endpoint_sg_id = module.networking.vpc_endpoint_security_group_id + vpc_id = module.networking.vpc_id } module "storage" { diff --git a/infrastructure/staging/terraform.tfvars.example b/infrastructure/staging/terraform.tfvars.example index 7c99d21948..d741e57deb 100644 --- a/infrastructure/staging/terraform.tfvars.example +++ b/infrastructure/staging/terraform.tfvars.example @@ -1,11 +1,10 @@ -availability_zones = ["us-east-2a", "us-east-2b", "us-east-2c"] -aws_region = "us-east-2" -create_nat_gateway = false -create_rds_proxy = false -create_vpc_endpoints = false -ecs_use_public_subnets = true -environment = "staging" -frontend_domain_name = https://nest.owasp.dev -frontend_use_fargate_spot = true -project_name = "owasp-nest" -use_fargate_spot = true +availability_zones = ["us-east-2a", "us-east-2b", "us-east-2c"] +aws_region = "us-east-2" +create_nat_gateway = true +create_rds_proxy = false +ecs_use_public_subnets = true +environment = "staging" +frontend_domain_name = "https://nest.owasp.dev" +frontend_use_fargate_spot = true +project_name = "owasp-nest" +use_fargate_spot = true diff --git a/infrastructure/staging/variables.tf b/infrastructure/staging/variables.tf index 17b8f96237..1389fa6660 100644 --- a/infrastructure/staging/variables.tf +++ b/infrastructure/staging/variables.tf @@ -13,7 +13,7 @@ variable "availability_zones" { variable "create_nat_gateway" { description = "Whether to create a NAT Gateway for private subnet internet access." type = bool - default = false + default = true } variable "create_rds_proxy" { @@ -22,8 +22,38 @@ variable "create_rds_proxy" { default = false } -variable "create_vpc_endpoints" { - description = "Whether to create VPC Endpoints for AWS services." +variable "create_vpc_cloudwatch_logs_endpoint" { + description = "Whether to create CloudWatch Logs VPC endpoint." + type = bool + default = false +} + +variable "create_vpc_ecr_api_endpoint" { + description = "Whether to create ECR API VPC endpoint." + type = bool + default = false +} + +variable "create_vpc_ecr_dkr_endpoint" { + description = "Whether to create ECR DKR VPC endpoint." + type = bool + default = false +} + +variable "create_vpc_s3_endpoint" { + description = "Whether to create S3 VPC endpoint (Gateway, free)." + type = bool + default = true +} + +variable "create_vpc_secretsmanager_endpoint" { + description = "Whether to create Secrets Manager VPC endpoint." + type = bool + default = false +} + +variable "create_vpc_ssm_endpoint" { + description = "Whether to create SSM VPC endpoint." type = bool default = false } From 38db5bea7576988ecc18ba6fdc146abea637e5e6 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Tue, 6 Jan 2026 21:41:23 +0530 Subject: [PATCH 08/25] Simplify README --- infrastructure/README.md | 91 ++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 54 deletions(-) diff --git a/infrastructure/README.md b/infrastructure/README.md index 5c620bf36d..449d267bfd 100644 --- a/infrastructure/README.md +++ b/infrastructure/README.md @@ -111,9 +111,6 @@ The Django backend deployment is managed by Zappa. This includes the API Gateway cd ../../backend/ ``` - > [!NOTE] - > The following steps assume the current working directory is `backend/` - 2. **Setup Dependencies**: - This step may differ for different operating systems. @@ -156,11 +153,10 @@ The Django backend deployment is managed by Zappa. This includes the API Gateway Once deployed, use the URL provided by Zappa to test the API. -## Setup Database +## Populate ECR Repositories +ECR Repositories are used to store images used by ECS (Frontend + Backend Tasks) -Migrate and load data into the new database. - -1. **Setup ECR Image**: +1. **Login to ECR**: - Login to the Elastic Container Registry using the following command: @@ -174,10 +170,12 @@ Migrate and load data into the new database. aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 000000000000.dkr.ecr.us-east-2.amazonaws.com ``` +2. **Uplaod backend image to ECR**: + - Build the backend image using the following command: ```bash - docker build -t owasp-nest-staging-backend:latest -f docker/Dockerfile . + docker build -t owasp-nest-staging-backend:latest -f docker/backend/Dockerfile backend/ ``` - Tag the image: @@ -198,48 +196,16 @@ Migrate and load data into the new database. docker push 000000000000.dkr.ecr.us-east-2.amazonaws.com/owasp-nest-staging-backend:latest ``` -2. **Upload Fixture to S3**: - - - Upload the fixture present in `backend/data` to `nest-fixtures` bucket using the following command: - - ```bash - aws s3 cp data/nest.json.gz s3://owasp-nest-fixtures-/ - ``` - -3. **Run ECS Tasks**: - - - Head over to Elastic Container Service in the AWS Console. - - Click on `owasp-nest-staging-migrate` in `Task Definitions` section. - - Select the task definition revision. - - Click Deploy > Run Task. - - Use the following configuration: - - Environment: Cluster: owasp-nest-staging-tasks-cluster - - Networking: - - VPC: owasp-nest-staging-vpc - - Subnets: subnets will be auto-selected due to VPC selection. - - Security group name: select the ECS security group (e.g. `owasp-nest-staging-ecs-sg`). - - Click "Create" - - The task is now running... Click on the task ID to view Logs, Status, etc. - - Follow the same steps for `owasp-nest-staging-load-data` and `owasp-nest-staging-index-data`. - -### Setup Frontend - -1. **Setup Frontend Image**: - - - Change the directory to `frontend/` using the following command: - - ```bash - cd frontend/ - ``` +3. **Uplaod frontend image to ECR**: - Build the frontend image using the following command: > [!NOTE] - > Make sure to update the `.env` file with correct `NEXT_PUBLIC_*` variables. + > Make sure to update the frontend `.env` file with correct `NEXT_PUBLIC_*` variables. > These are injected at build time. ```bash - docker build -t owasp-nest-staging-frontend:latest -f docker/Dockerfile . + docker build -t owasp-nest-staging-frontend:latest -f docker/frontend/Dockerfile frontend/ ``` - Tag the image: @@ -260,21 +226,36 @@ Migrate and load data into the new database. docker push 000000000000.dkr.ecr.us-east-2.amazonaws.com/owasp-nest-staging-frontend:latest ``` -2. **Deploy Frontend Infrastructure**: +## Setup Database +Migrate and load data into the new database. - > [!IMPORTANT] - > Make sure to push the frontend Docker image before running `terraform apply`, as it runs frontend ECS tasks. +1. **Upload Fixture to S3**: - - Run Terraform apply: + - Upload the fixture present in `backend/data` to `nest-fixtures` bucket using the following command: ```bash - terraform apply + aws s3 cp data/nest.json.gz s3://owasp-nest-fixtures-/ ``` - > [!NOTE] - > On first apply, there may be an error 400 when creating the HTTPS Listener for ALB. This is expected because the ACM certificate is not yet validated. +2. **Run ECS Tasks**: -3. **Validate ACM Certificate**: + - Head over to Elastic Container Service in the AWS Console. + - Click on `owasp-nest-staging-migrate` in `Task Definitions` section. + - Select the task definition revision. + - Click Deploy > Run Task. + - Use the following configuration: + - Environment: Cluster: owasp-nest-staging-tasks-cluster + - Networking: + - VPC: owasp-nest-staging-vpc + - Subnets: subnets will be auto-selected due to VPC selection. + - Security group name: select the ECS security group (e.g. `owasp-nest-staging-ecs-sg`). + - Click "Create" + - The task is now running... Click on the task ID to view Logs, Status, etc. + - Follow the same steps for `owasp-nest-staging-load-data` and `owasp-nest-staging-index-data`. + +## Configure Domain and Frontend + +1. **Validate ACM Certificate**: - Get the DNS validation records: @@ -290,11 +271,13 @@ Migrate and load data into the new database. terraform apply ``` -4. **Configure Frontend Parameters**: + - Add a CNAME record and point the domain to the frontend ALB. + +2. **Configure Frontend Parameters**: - - Update the frontend server parameters using the Lambda URL from Terraform outputs. + - Update the frontend server (`NEXT_SERVER_*`) parameters using the Lambda URL from Terraform outputs. -5. **Restart Frontend ECS Tasks**: +3. **Restart Frontend ECS Tasks**: - Force a new deployment to pick up the updated configuration: From a086c70aac50941e6f6d74718b58eb64ebe364c0 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Tue, 6 Jan 2026 23:32:44 +0530 Subject: [PATCH 09/25] Refactor alb to be a module --- infrastructure/README.md | 2 +- infrastructure/modules/alb/main.tf | 193 ++++++++++++++++++ infrastructure/modules/alb/outputs.tf | 39 ++++ infrastructure/modules/alb/variables.tf | 60 ++++++ infrastructure/modules/frontend/main.tf | 29 +-- infrastructure/modules/frontend/outputs.tf | 36 ---- infrastructure/modules/frontend/variables.tf | 44 +--- .../networking/modules/vpc-endpoint/main.tf | 1 - infrastructure/staging/main.tf | 25 ++- infrastructure/staging/outputs.tf | 35 ++-- 10 files changed, 335 insertions(+), 129 deletions(-) create mode 100644 infrastructure/modules/alb/main.tf create mode 100644 infrastructure/modules/alb/outputs.tf create mode 100644 infrastructure/modules/alb/variables.tf diff --git a/infrastructure/README.md b/infrastructure/README.md index 449d267bfd..a0e6883ad9 100644 --- a/infrastructure/README.md +++ b/infrastructure/README.md @@ -260,7 +260,7 @@ Migrate and load data into the new database. - Get the DNS validation records: ```bash - terraform output frontend_acm_validation_records + terraform output acm_certificate_domain_validation_options ``` - Add the CNAME records to your DNS provider. diff --git a/infrastructure/modules/alb/main.tf b/infrastructure/modules/alb/main.tf new file mode 100644 index 0000000000..4264ce0fce --- /dev/null +++ b/infrastructure/modules/alb/main.tf @@ -0,0 +1,193 @@ +terraform { + required_version = "1.14.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "6.22.0" + } + random = { + source = "hashicorp/random" + version = "3.7.2" + } + } +} + +data "aws_elb_service_account" "main" {} + +data "aws_iam_policy_document" "alb_logs" { + statement { + actions = ["s3:PutObject"] + effect = "Allow" + resources = ["${aws_s3_bucket.alb_logs.arn}/*"] + sid = "AllowALBLogDelivery" + + principals { + identifiers = [data.aws_elb_service_account.main.arn] + type = "AWS" + } + } +} + +resource "aws_acm_certificate" "main" { + count = var.enable_https && var.domain_name != null ? 1 : 0 + domain_name = var.domain_name + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-alb-cert" + }) + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_lb" "main" { + depends_on = [aws_s3_bucket_policy.alb_logs] + enable_deletion_protection = false + internal = false + load_balancer_type = "application" + name = "${var.project_name}-${var.environment}-alb" + security_groups = [var.alb_sg_id] + subnets = var.public_subnet_ids + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-alb" + }) + + access_logs { + bucket = aws_s3_bucket.alb_logs.id + enabled = true + prefix = "alb" + } +} + +resource "aws_lb_listener" "http" { + count = var.enable_https ? 0 : 1 + load_balancer_arn = aws_lb.main.arn + port = 80 + protocol = "HTTP" #NOSONAR + tags = var.common_tags + + default_action { + target_group_arn = aws_lb_target_group.frontend.arn + type = "forward" + } +} + +resource "aws_lb_listener" "http_redirect" { + count = var.enable_https ? 1 : 0 + load_balancer_arn = aws_lb.main.arn + port = 80 + protocol = "HTTP" #NOSONAR + tags = var.common_tags + + default_action { + type = "redirect" + + redirect { + port = "443" + protocol = "HTTPS" + status_code = "HTTP_301" + } + } +} + +resource "aws_lb_listener" "https" { + certificate_arn = var.enable_https ? aws_acm_certificate.main[0].arn : null + count = var.enable_https ? 1 : 0 + load_balancer_arn = aws_lb.main.arn + port = 443 + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" + tags = var.common_tags + + default_action { + target_group_arn = aws_lb_target_group.frontend.arn + type = "forward" + } +} + +resource "aws_lb_target_group" "frontend" { + deregistration_delay = 30 + name = "${var.project_name}-${var.environment}-frontend-tg" + port = var.frontend_port + protocol = "HTTP" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-frontend-tg" + }) + target_type = "ip" + vpc_id = var.vpc_id + + health_check { + enabled = true + healthy_threshold = 2 + interval = 30 + matcher = "200-299" + path = var.frontend_health_check_path + protocol = "HTTP" + timeout = 5 + unhealthy_threshold = 3 + } +} + +resource "aws_s3_bucket" "alb_logs" { # NOSONAR + bucket = "${var.project_name}-${var.environment}-alb-logs-${random_id.suffix.hex}" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-alb-logs" + }) + + lifecycle { + prevent_destroy = true + } +} + +resource "aws_s3_bucket_lifecycle_configuration" "alb_logs" { + bucket = aws_s3_bucket.alb_logs.id + + rule { + id = "expire-logs" + status = "Enabled" + + abort_incomplete_multipart_upload { + days_after_initiation = 7 + } + expiration { + days = var.log_retention_days + } + } +} + +resource "aws_s3_bucket_policy" "alb_logs" { + bucket = aws_s3_bucket.alb_logs.id + policy = data.aws_iam_policy_document.alb_logs.json +} + +resource "aws_s3_bucket_public_access_block" "alb_logs" { + block_public_acls = true + block_public_policy = true + bucket = aws_s3_bucket.alb_logs.id + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "alb_logs" { + bucket = aws_s3_bucket.alb_logs.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_versioning" "alb_logs" { + bucket = aws_s3_bucket.alb_logs.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "random_id" "suffix" { + byte_length = 4 +} diff --git a/infrastructure/modules/alb/outputs.tf b/infrastructure/modules/alb/outputs.tf new file mode 100644 index 0000000000..5300a407de --- /dev/null +++ b/infrastructure/modules/alb/outputs.tf @@ -0,0 +1,39 @@ +output "acm_certificate_arn" { + description = "The ARN of the ACM certificate." + value = var.enable_https && var.domain_name != null ? aws_acm_certificate.main[0].arn : null +} + +output "acm_certificate_domain_validation_options" { + description = "The domain validation options for ACM certificate DNS validation." + value = var.enable_https && var.domain_name != null ? aws_acm_certificate.main[0].domain_validation_options : [] +} + +output "alb_arn" { + description = "The ARN of the ALB." + value = aws_lb.main.arn +} + +output "alb_dns_name" { + description = "The DNS name of the ALB." + value = aws_lb.main.dns_name +} + +output "alb_zone_id" { + description = "The zone ID of the ALB." + value = aws_lb.main.zone_id +} + +output "frontend_target_group_arn" { + description = "The ARN of the frontend target group." + value = aws_lb_target_group.frontend.arn +} + +output "http_listener_arn" { + description = "The ARN of the HTTP listener." + value = var.enable_https ? aws_lb_listener.http_redirect[0].arn : aws_lb_listener.http[0].arn +} + +output "https_listener_arn" { + description = "The ARN of the HTTPS listener (null if HTTPS disabled)." + value = var.enable_https ? aws_lb_listener.https[0].arn : null +} diff --git a/infrastructure/modules/alb/variables.tf b/infrastructure/modules/alb/variables.tf new file mode 100644 index 0000000000..fe0e17e634 --- /dev/null +++ b/infrastructure/modules/alb/variables.tf @@ -0,0 +1,60 @@ +variable "alb_sg_id" { + description = "The security group ID for the ALB." + type = string +} + +variable "common_tags" { + description = "A map of common tags to apply to all resources." + type = map(string) + default = {} +} + +variable "domain_name" { + description = "The domain name for ACM certificate (e.g., nest.owasp.dev)." + type = string + default = null +} + +variable "enable_https" { + description = "Whether to enable the HTTPS listener." + type = bool + default = false +} + +variable "environment" { + description = "The environment name (e.g., staging, production)." + type = string +} + +variable "frontend_health_check_path" { + description = "The health check path for the frontend target group." + type = string + default = "/" +} + +variable "frontend_port" { + description = "The port for the frontend target group." + type = number + default = 3000 +} + +variable "log_retention_days" { + description = "The number of days to retain ALB access logs." + type = number + default = 90 +} + +variable "project_name" { + description = "The name of the project." + type = string +} + +variable "public_subnet_ids" { + description = "A list of public subnet IDs for the ALB." + type = list(string) +} + +variable "vpc_id" { + description = "The ID of the VPC." + type = string +} diff --git a/infrastructure/modules/frontend/main.tf b/infrastructure/modules/frontend/main.tf index b921a54247..de379aaadd 100644 --- a/infrastructure/modules/frontend/main.tf +++ b/infrastructure/modules/frontend/main.tf @@ -11,33 +11,6 @@ terraform { data "aws_caller_identity" "current" {} -module "alb" { - source = "./modules/alb" - - acm_certificate_arn = var.enable_https && var.domain_name != null ? aws_acm_certificate.frontend[0].arn : null - alb_sg_id = var.alb_sg_id - common_tags = var.common_tags - enable_https = var.enable_https - environment = var.environment - health_check_path = var.health_check_path - project_name = var.project_name - public_subnet_ids = var.public_subnet_ids - vpc_id = var.vpc_id -} - -resource "aws_acm_certificate" "frontend" { - count = var.enable_https && var.domain_name != null ? 1 : 0 - domain_name = var.domain_name - tags = merge(var.common_tags, { - Name = "${var.project_name}-${var.environment}-frontend-cert" - }) - validation_method = "DNS" - - lifecycle { - create_before_destroy = true - } -} - resource "aws_appautoscaling_policy" "frontend_cpu" { count = var.enable_auto_scaling ? 1 : 0 name = "${var.project_name}-${var.environment}-frontend-cpu-scaling" @@ -139,7 +112,7 @@ resource "aws_ecs_service" "frontend" { load_balancer { container_name = "frontend" container_port = 3000 - target_group_arn = module.alb.target_group_arn + target_group_arn = var.target_group_arn } network_configuration { diff --git a/infrastructure/modules/frontend/outputs.tf b/infrastructure/modules/frontend/outputs.tf index 63fc98bdd0..dbf5add28c 100644 --- a/infrastructure/modules/frontend/outputs.tf +++ b/infrastructure/modules/frontend/outputs.tf @@ -1,34 +1,3 @@ -output "acm_certificate_arn" { - description = "The ARN of the ACM certificate." - value = var.enable_https && var.domain_name != null ? aws_acm_certificate.frontend[0].arn : null -} - -output "acm_certificate_status" { - description = "The status of the ACM certificate." - value = var.enable_https && var.domain_name != null ? aws_acm_certificate.frontend[0].status : null -} - -output "acm_validation_records" { - description = "The DNS validation records to add to your DNS provider." - value = var.enable_https && var.domain_name != null ? { - for domain_validation_option in aws_acm_certificate.frontend[0].domain_validation_options : domain_validation_option.domain_name => { - name = domain_validation_option.resource_record_name - type = domain_validation_option.resource_record_type - value = domain_validation_option.resource_record_value - } - } : {} -} - -output "alb_dns_name" { - description = "The DNS name of the frontend ALB." - value = module.alb.alb_dns_name -} - -output "alb_zone_id" { - description = "The zone ID of the frontend ALB." - value = module.alb.alb_zone_id -} - output "ecr_repository_url" { description = "The URL of the frontend ECR repository." value = aws_ecr_repository.frontend.repository_url @@ -48,8 +17,3 @@ output "ecs_service_name" { description = "The name of the ECS service." value = aws_ecs_service.frontend.name } - -output "target_group_arn" { - description = "The ARN of the target group." - value = module.alb.target_group_arn -} diff --git a/infrastructure/modules/frontend/variables.tf b/infrastructure/modules/frontend/variables.tf index dc3115f342..c9e949e2df 100644 --- a/infrastructure/modules/frontend/variables.tf +++ b/infrastructure/modules/frontend/variables.tf @@ -1,15 +1,10 @@ -variable "alb_sg_id" { - description = "The security group ID for the ALB." - type = string -} - variable "aws_region" { description = "The AWS region for resources." type = string } variable "common_tags" { - description = "The common tags to apply to all resources." + description = "A map of common tags to apply to all resources." type = map(string) default = {} } @@ -32,26 +27,14 @@ variable "desired_count" { default = 2 } -variable "domain_name" { - description = "The domain name for the frontend (required for HTTPS)." - type = string - default = null -} - variable "enable_auto_scaling" { description = "Whether to enable auto scaling for the frontend." type = bool default = false } -variable "enable_https" { - description = "Whether to enable the HTTPS listener on the ALB." - type = bool - default = false -} - variable "environment" { - description = "The environment name (staging, production)." + description = "The environment name (e.g., staging, production)." type = string } @@ -65,12 +48,6 @@ variable "frontend_sg_id" { type = string } -variable "health_check_path" { - description = "The health check path for the frontend." - type = string - default = "/" -} - variable "image_tag" { description = "The Docker image tag for the frontend." type = string @@ -96,27 +73,22 @@ variable "min_count" { } variable "private_subnet_ids" { - description = "The list of private subnet IDs for ECS tasks." + description = "A list of private subnet IDs for ECS tasks." type = list(string) } variable "project_name" { - description = "The project name." + description = "The name of the project." type = string } -variable "public_subnet_ids" { - description = "The list of public subnet IDs for the ALB." - type = list(string) +variable "target_group_arn" { + description = "The ARN of the ALB target group for the frontend." + type = string } variable "use_fargate_spot" { - description = "Use Fargate Spot capacity provider for frontend tasks." + description = "Whether to use Fargate Spot capacity provider for frontend tasks." type = bool default = false } - -variable "vpc_id" { - description = "The VPC ID." - type = string -} diff --git a/infrastructure/modules/networking/modules/vpc-endpoint/main.tf b/infrastructure/modules/networking/modules/vpc-endpoint/main.tf index bee698da64..e86102b1fe 100644 --- a/infrastructure/modules/networking/modules/vpc-endpoint/main.tf +++ b/infrastructure/modules/networking/modules/vpc-endpoint/main.tf @@ -10,7 +10,6 @@ terraform { } locals { - # Check if any interface endpoints are enabled (need security group) any_interface_endpoint = var.create_cloudwatch_logs || var.create_ecr_api || var.create_ecr_dkr || var.create_secretsmanager || var.create_ssm } diff --git a/infrastructure/staging/main.tf b/infrastructure/staging/main.tf index 339d9e1e9f..0ec8cbd395 100644 --- a/infrastructure/staging/main.tf +++ b/infrastructure/staging/main.tf @@ -17,6 +17,21 @@ locals { } } +module "alb" { + source = "../modules/alb" + + alb_sg_id = module.security.alb_sg_id + common_tags = local.common_tags + domain_name = var.frontend_domain_name + enable_https = var.frontend_domain_name != null + environment = var.environment + frontend_health_check_path = "/" + frontend_port = 3000 + project_name = var.project_name + public_subnet_ids = module.networking.public_subnet_ids + vpc_id = module.networking.vpc_id +} + module "cache" { source = "../modules/cache" @@ -73,13 +88,10 @@ module "ecs" { module "frontend" { source = "../modules/frontend" - alb_sg_id = module.security.alb_sg_id aws_region = var.aws_region common_tags = local.common_tags desired_count = var.frontend_desired_count - domain_name = var.frontend_domain_name enable_auto_scaling = var.frontend_enable_auto_scaling - enable_https = var.frontend_domain_name != null environment = var.environment frontend_parameters_arns = module.parameters.frontend_ssm_parameter_arns frontend_sg_id = module.security.frontend_sg_id @@ -87,9 +99,8 @@ module "frontend" { min_count = var.frontend_min_count private_subnet_ids = module.networking.private_subnet_ids project_name = var.project_name - public_subnet_ids = module.networking.public_subnet_ids + target_group_arn = module.alb.frontend_target_group_arn use_fargate_spot = var.frontend_use_fargate_spot - vpc_id = module.networking.vpc_id } module "networking" { @@ -115,7 +126,7 @@ module "networking" { module "parameters" { source = "../modules/parameters" - allowed_origins = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.frontend.alb_dns_name}" + allowed_origins = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.alb.alb_dns_name}" common_tags = local.common_tags db_host = module.database.db_proxy_endpoint db_name = var.db_name @@ -123,7 +134,7 @@ module "parameters" { db_port = var.db_port db_user = var.db_user environment = var.environment - nextauth_url = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.frontend.alb_dns_name}" + nextauth_url = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.alb.alb_dns_name}" project_name = var.project_name redis_host = module.cache.redis_primary_endpoint redis_password_arn = module.cache.redis_password_arn diff --git a/infrastructure/staging/outputs.tf b/infrastructure/staging/outputs.tf index d58c64ef15..2f12c61948 100644 --- a/infrastructure/staging/outputs.tf +++ b/infrastructure/staging/outputs.tf @@ -1,21 +1,16 @@ -output "backend_ecr_repository_url" { - description = "The URL of the backend ECR repository." - value = module.ecs.ecr_repository_url -} - -output "frontend_acm_certificate_status" { - description = "The status of the frontend ACM certificate" - value = module.frontend.acm_certificate_status +output "acm_certificate_domain_validation_options" { + description = "The DNS validation options for ACM certificate." + value = module.alb.acm_certificate_domain_validation_options } -output "frontend_acm_validation_records" { - description = "The DNS validation records to add to the DNS provider for HTTPS." - value = module.frontend.acm_validation_records +output "alb_dns_name" { + description = "The DNS name of the ALB." + value = module.alb.alb_dns_name } -output "frontend_alb_dns_name" { - description = "The DNS name of the frontend ALB." - value = module.frontend.alb_dns_name +output "backend_ecr_repository_url" { + description = "The URL of the backend ECR repository." + value = module.ecs.ecr_repository_url } output "frontend_ecr_repository_url" { @@ -25,12 +20,7 @@ output "frontend_ecr_repository_url" { output "frontend_url" { description = "The URL to access the frontend." - value = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.frontend.alb_dns_name}" -} - -output "private_subnet_ids" { - description = "A list of private subnet IDs." - value = module.networking.private_subnet_ids + value = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.alb.alb_dns_name}" } output "lambda_security_group_id" { @@ -38,6 +28,11 @@ output "lambda_security_group_id" { value = module.security.lambda_sg_id } +output "private_subnet_ids" { + description = "A list of private subnet IDs." + value = module.networking.private_subnet_ids +} + output "zappa_s3_bucket" { description = "The name of the S3 bucket for Zappa deployments." value = module.storage.zappa_s3_bucket.bucket From b633c051469a20228f150824c447c62c947345dd Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Tue, 6 Jan 2026 23:44:54 +0530 Subject: [PATCH 10/25] Add backend to ALB --- infrastructure/modules/alb/main.tf | 93 +++++++++++++++++++++++++ infrastructure/modules/alb/outputs.tf | 5 ++ infrastructure/modules/alb/variables.tf | 12 ++++ infrastructure/staging/main.tf | 2 + infrastructure/staging/variables.tf | 12 ++++ 5 files changed, 124 insertions(+) diff --git a/infrastructure/modules/alb/main.tf b/infrastructure/modules/alb/main.tf index 4264ce0fce..a308e7b953 100644 --- a/infrastructure/modules/alb/main.tf +++ b/infrastructure/modules/alb/main.tf @@ -130,6 +130,99 @@ resource "aws_lb_target_group" "frontend" { } } +resource "aws_lb_target_group" "lambda" { + count = var.lambda_arn != null ? 1 : 0 + name = "${var.project_name}-${var.environment}-lambda-tg" + target_type = "lambda" + tags = merge(var.common_tags, { + Name = "${var.project_name}-${var.environment}-lambda-tg" + }) + + health_check { + enabled = true + healthy_threshold = 2 + interval = 35 + matcher = "200-299" + path = "/status/" + timeout = 30 + unhealthy_threshold = 3 + } +} + +resource "aws_lb_target_group_attachment" "lambda" { + count = var.lambda_arn != null ? 1 : 0 + target_group_arn = aws_lb_target_group.lambda[0].arn + target_id = var.lambda_arn + depends_on = [aws_lambda_permission.alb] +} + +resource "aws_lambda_permission" "alb" { + count = var.lambda_arn != null ? 1 : 0 + action = "lambda:InvokeFunction" + function_name = var.lambda_function_name + principal = "elasticloadbalancing.amazonaws.com" + source_arn = aws_lb_target_group.lambda[0].arn + statement_id = "AllowALBInvoke" +} + +resource "aws_lb_listener_rule" "backend_https" { + count = var.lambda_arn != null && var.enable_https ? 1 : 0 + listener_arn = aws_lb_listener.https[0].arn + priority = 100 + tags = var.common_tags + + action { + type = "forward" + target_group_arn = aws_lb_target_group.lambda[0].arn + } + + condition { + path_pattern { + values = [ + "/a/*", + "/api/*", + "/csrf/*", + "/graphql", + "/graphql/*", + "/idx/*", + "/integrations/*", + "/sitemap", + "/sitemap.xml", + "/status/*" + ] + } + } +} + +resource "aws_lb_listener_rule" "backend_http" { + count = var.lambda_arn != null && !var.enable_https ? 1 : 0 + listener_arn = aws_lb_listener.http[0].arn + priority = 100 + tags = var.common_tags + + action { + type = "forward" + target_group_arn = aws_lb_target_group.lambda[0].arn + } + + condition { + path_pattern { + values = [ + "/a/*", + "/api/*", + "/csrf/*", + "/graphql", + "/graphql/*", + "/idx/*", + "/integrations/*", + "/sitemap", + "/sitemap.xml", + "/status/*" + ] + } + } +} + resource "aws_s3_bucket" "alb_logs" { # NOSONAR bucket = "${var.project_name}-${var.environment}-alb-logs-${random_id.suffix.hex}" tags = merge(var.common_tags, { diff --git a/infrastructure/modules/alb/outputs.tf b/infrastructure/modules/alb/outputs.tf index 5300a407de..ee3c4d4481 100644 --- a/infrastructure/modules/alb/outputs.tf +++ b/infrastructure/modules/alb/outputs.tf @@ -37,3 +37,8 @@ output "https_listener_arn" { description = "The ARN of the HTTPS listener (null if HTTPS disabled)." value = var.enable_https ? aws_lb_listener.https[0].arn : null } + +output "lambda_target_group_arn" { + description = "The ARN of the Lambda target group (null if Lambda not configured)." + value = var.lambda_arn != null ? aws_lb_target_group.lambda[0].arn : null +} diff --git a/infrastructure/modules/alb/variables.tf b/infrastructure/modules/alb/variables.tf index fe0e17e634..a7ad5b6d08 100644 --- a/infrastructure/modules/alb/variables.tf +++ b/infrastructure/modules/alb/variables.tf @@ -26,6 +26,18 @@ variable "environment" { type = string } +variable "lambda_arn" { + description = "The ARN of the Lambda function for backend routing (null to skip Lambda routing)." + type = string + default = null +} + +variable "lambda_function_name" { + description = "The name of the Lambda function for backend routing." + type = string + default = null +} + variable "frontend_health_check_path" { description = "The health check path for the frontend target group." type = string diff --git a/infrastructure/staging/main.tf b/infrastructure/staging/main.tf index 0ec8cbd395..4f66828e67 100644 --- a/infrastructure/staging/main.tf +++ b/infrastructure/staging/main.tf @@ -27,6 +27,8 @@ module "alb" { environment = var.environment frontend_health_check_path = "/" frontend_port = 3000 + lambda_arn = var.lambda_arn + lambda_function_name = var.lambda_function_name project_name = var.project_name public_subnet_ids = module.networking.public_subnet_ids vpc_id = module.networking.vpc_id diff --git a/infrastructure/staging/variables.tf b/infrastructure/staging/variables.tf index 1389fa6660..a8b50cb5b0 100644 --- a/infrastructure/staging/variables.tf +++ b/infrastructure/staging/variables.tf @@ -16,6 +16,18 @@ variable "create_nat_gateway" { default = true } +variable "lambda_arn" { + description = "The ARN of the Zappa Lambda function for backend routing." + type = string + default = null +} + +variable "lambda_function_name" { + description = "The name of the Zappa Lambda function." + type = string + default = null +} + variable "create_rds_proxy" { description = "Whether to create an RDS proxy." type = bool From 2f33a50255803932d4c19a662e9a4a07888df765 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Wed, 7 Jan 2026 00:07:22 +0530 Subject: [PATCH 11/25] split routes --- backend/zappa_settings.example.json | 1 + infrastructure/README.md | 18 ++++- infrastructure/modules/alb/main.tf | 66 +++++++++++-------- infrastructure/modules/alb/outputs.tf | 5 ++ infrastructure/staging/outputs.tf | 5 ++ .../staging/terraform.tfvars.example | 2 + 6 files changed, 69 insertions(+), 28 deletions(-) diff --git a/backend/zappa_settings.example.json b/backend/zappa_settings.example.json index 68d682c6d6..fac9dd3f61 100644 --- a/backend/zappa_settings.example.json +++ b/backend/zappa_settings.example.json @@ -1,5 +1,6 @@ { "staging": { + "apigateway_enabled": false, "app_function": "wsgi.application", "aws_environment_variables": { "AWS_SYSTEMS_MANAGER_PARAM_STORE_PATH": "/owasp-nest/staging" diff --git a/infrastructure/README.md b/infrastructure/README.md index a0e6883ad9..b691a36682 100644 --- a/infrastructure/README.md +++ b/infrastructure/README.md @@ -101,7 +101,7 @@ Follow these steps to set up the infrastructure: ## Setting up Zappa -The Django backend deployment is managed by Zappa. This includes the API Gateway, IAM roles, and Lambda Function provision. +The Django backend deployment is managed by Zappa. This includes the IAM roles, and Lambda Function provision. 1. **Change Directory**: @@ -151,7 +151,21 @@ The Django backend deployment is managed by Zappa. This includes the API Gateway > [!NOTE] > If the deployment is successful but returns a `5xx` error, resolve the issues and use `zappa undeploy staging` & `zappa deploy staging`. The command `zappa update staging` may not work. - Once deployed, use the URL provided by Zappa to test the API. +6. **Configure ALB Routing**: + - Run `zappa status staging` to get Zappa details. + - Update `terraform.tfvars` with the Lambda details: + + ```hcl + lambda_arn = "arn:aws:lambda:us-east-2:000000000000:function:nest-backend-staging" + lambda_function_name = "nest-backend-staging" + ``` + + - Apply the changes to create ALB routing: + + ```bash + cd ../infrastructure/staging/ + terraform apply + ``` ## Populate ECR Repositories ECR Repositories are used to store images used by ECS (Frontend + Backend Tasks) diff --git a/infrastructure/modules/alb/main.tf b/infrastructure/modules/alb/main.tf index a308e7b953..0ad4b882c2 100644 --- a/infrastructure/modules/alb/main.tf +++ b/infrastructure/modules/alb/main.tf @@ -165,7 +165,7 @@ resource "aws_lambda_permission" "alb" { statement_id = "AllowALBInvoke" } -resource "aws_lb_listener_rule" "backend_https" { +resource "aws_lb_listener_rule" "backend_https_1" { count = var.lambda_arn != null && var.enable_https ? 1 : 0 listener_arn = aws_lb_listener.https[0].arn priority = 100 @@ -178,23 +178,30 @@ resource "aws_lb_listener_rule" "backend_https" { condition { path_pattern { - values = [ - "/a/*", - "/api/*", - "/csrf/*", - "/graphql", - "/graphql/*", - "/idx/*", - "/integrations/*", - "/sitemap", - "/sitemap.xml", - "/status/*" - ] + values = ["/a/*", "/api/*", "/csrf/*", "/graphql", "/graphql/*"] } } } -resource "aws_lb_listener_rule" "backend_http" { +resource "aws_lb_listener_rule" "backend_https_2" { + count = var.lambda_arn != null && var.enable_https ? 1 : 0 + listener_arn = aws_lb_listener.https[0].arn + priority = 101 + tags = var.common_tags + + action { + type = "forward" + target_group_arn = aws_lb_target_group.lambda[0].arn + } + + condition { + path_pattern { + values = ["/idx/*", "/integrations/*", "/sitemap", "/sitemap.xml", "/status/*"] + } + } +} + +resource "aws_lb_listener_rule" "backend_http_1" { count = var.lambda_arn != null && !var.enable_https ? 1 : 0 listener_arn = aws_lb_listener.http[0].arn priority = 100 @@ -207,18 +214,25 @@ resource "aws_lb_listener_rule" "backend_http" { condition { path_pattern { - values = [ - "/a/*", - "/api/*", - "/csrf/*", - "/graphql", - "/graphql/*", - "/idx/*", - "/integrations/*", - "/sitemap", - "/sitemap.xml", - "/status/*" - ] + values = ["/a/*", "/api/*", "/csrf/*", "/graphql", "/graphql/*"] + } + } +} + +resource "aws_lb_listener_rule" "backend_http_2" { + count = var.lambda_arn != null && !var.enable_https ? 1 : 0 + listener_arn = aws_lb_listener.http[0].arn + priority = 101 + tags = var.common_tags + + action { + type = "forward" + target_group_arn = aws_lb_target_group.lambda[0].arn + } + + condition { + path_pattern { + values = ["/idx/*", "/integrations/*", "/sitemap", "/sitemap.xml", "/status/*"] } } } diff --git a/infrastructure/modules/alb/outputs.tf b/infrastructure/modules/alb/outputs.tf index ee3c4d4481..cfdb1d3537 100644 --- a/infrastructure/modules/alb/outputs.tf +++ b/infrastructure/modules/alb/outputs.tf @@ -8,6 +8,11 @@ output "acm_certificate_domain_validation_options" { value = var.enable_https && var.domain_name != null ? aws_acm_certificate.main[0].domain_validation_options : [] } +output "acm_certificate_status" { + description = "The status of the ACM certificate." + value = var.enable_https && var.domain_name != null ? aws_acm_certificate.main[0].status : null +} + output "alb_arn" { description = "The ARN of the ALB." value = aws_lb.main.arn diff --git a/infrastructure/staging/outputs.tf b/infrastructure/staging/outputs.tf index 2f12c61948..d372b17066 100644 --- a/infrastructure/staging/outputs.tf +++ b/infrastructure/staging/outputs.tf @@ -3,6 +3,11 @@ output "acm_certificate_domain_validation_options" { value = module.alb.acm_certificate_domain_validation_options } +output "acm_certificate_status" { + description = "The status of the ACM certificate." + value = module.alb.acm_certificate_status +} + output "alb_dns_name" { description = "The DNS name of the ALB." value = module.alb.alb_dns_name diff --git a/infrastructure/staging/terraform.tfvars.example b/infrastructure/staging/terraform.tfvars.example index d741e57deb..e15236d7e8 100644 --- a/infrastructure/staging/terraform.tfvars.example +++ b/infrastructure/staging/terraform.tfvars.example @@ -6,5 +6,7 @@ ecs_use_public_subnets = true environment = "staging" frontend_domain_name = "https://nest.owasp.dev" frontend_use_fargate_spot = true +lambda_arn = null +lambda_function_name = null project_name = "owasp-nest" use_fargate_spot = true From fb83528b730edc81a3c6129163bf73462187ce8f Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Wed, 7 Jan 2026 18:30:06 +0530 Subject: [PATCH 12/25] Update routes and remove code --- backend/settings/staging.py | 5 -- infrastructure/README.md | 6 ++ infrastructure/modules/alb/main.tf | 83 +++++++------------ infrastructure/modules/cache/main.tf | 2 +- infrastructure/modules/parameters/main.tf | 36 +------- infrastructure/modules/parameters/outputs.tf | 39 ++++----- .../modules/parameters/variables.tf | 3 +- infrastructure/staging/main.tf | 1 + 8 files changed, 57 insertions(+), 118 deletions(-) diff --git a/backend/settings/staging.py b/backend/settings/staging.py index 48e3be0f17..6f78dbe14f 100644 --- a/backend/settings/staging.py +++ b/backend/settings/staging.py @@ -45,11 +45,6 @@ class Staging(Base): CORS_ALLOWED_ORIGINS = ALLOWED_ORIGINS CSRF_TRUSTED_ORIGINS = ALLOWED_ORIGINS - CSRF_COOKIE_HTTPONLY = values.BooleanValue(environ_name="CSRF_COOKIE_HTTPONLY", default=False) - CSRF_COOKIE_SAMESITE = values.Value(environ_name="CSRF_COOKIE_SAMESITE", default="Lax") - CSRF_COOKIE_SECURE = True - SESSION_COOKIE_SAMESITE = values.Value(environ_name="SESSION_COOKIE_SAMESITE", default="Lax") - IS_STAGING_ENVIRONMENT = True SLACK_COMMANDS_ENABLED = True diff --git a/infrastructure/README.md b/infrastructure/README.md index b691a36682..9674a1dd53 100644 --- a/infrastructure/README.md +++ b/infrastructure/README.md @@ -338,3 +338,9 @@ Migrate and load data into the new database. ```bash zappa update staging ``` + +## Known Issues +There's a known issue with Zappa removing permissions and disconnecting the externally managed +API Gateway on each `update` or `deploy` action. +The temporary fix is to run `terraform apply` right after these actions. +Reference: https://github.com/zappa/Zappa/issues/939 diff --git a/infrastructure/modules/alb/main.tf b/infrastructure/modules/alb/main.tf index 0ad4b882c2..d274797851 100644 --- a/infrastructure/modules/alb/main.tf +++ b/infrastructure/modules/alb/main.tf @@ -13,6 +13,27 @@ terraform { } } +locals { + backend_paths = [ + "/a", + "/a/*", + "/api/*", + "/csrf", + "/csrf/*", + "/graphql", + "/graphql/*", + "/idx", + "/idx/*", + "/integrations", + "/integrations/*", + "/sitemap", + "/sitemap.xml", + "/status", + "/status/*", + ] + backend_path_chunks = chunklist(local.backend_paths, 5) +} + data "aws_elb_service_account" "main" {} data "aws_iam_policy_document" "alb_logs" { @@ -137,16 +158,6 @@ resource "aws_lb_target_group" "lambda" { tags = merge(var.common_tags, { Name = "${var.project_name}-${var.environment}-lambda-tg" }) - - health_check { - enabled = true - healthy_threshold = 2 - interval = 35 - matcher = "200-299" - path = "/status/" - timeout = 30 - unhealthy_threshold = 3 - } } resource "aws_lb_target_group_attachment" "lambda" { @@ -165,74 +176,38 @@ resource "aws_lambda_permission" "alb" { statement_id = "AllowALBInvoke" } -resource "aws_lb_listener_rule" "backend_https_1" { - count = var.lambda_arn != null && var.enable_https ? 1 : 0 +resource "aws_lb_listener_rule" "backend_https" { + for_each = var.lambda_arn != null && var.enable_https ? { for idx, chunk in local.backend_path_chunks : idx => chunk } : {} listener_arn = aws_lb_listener.https[0].arn - priority = 100 + priority = 100 + each.key tags = var.common_tags action { - type = "forward" target_group_arn = aws_lb_target_group.lambda[0].arn - } - - condition { - path_pattern { - values = ["/a/*", "/api/*", "/csrf/*", "/graphql", "/graphql/*"] - } - } -} - -resource "aws_lb_listener_rule" "backend_https_2" { - count = var.lambda_arn != null && var.enable_https ? 1 : 0 - listener_arn = aws_lb_listener.https[0].arn - priority = 101 - tags = var.common_tags - - action { type = "forward" - target_group_arn = aws_lb_target_group.lambda[0].arn } condition { path_pattern { - values = ["/idx/*", "/integrations/*", "/sitemap", "/sitemap.xml", "/status/*"] + values = each.value } } } -resource "aws_lb_listener_rule" "backend_http_1" { - count = var.lambda_arn != null && !var.enable_https ? 1 : 0 +resource "aws_lb_listener_rule" "backend_http" { + for_each = var.lambda_arn != null && !var.enable_https ? { for idx, chunk in local.backend_path_chunks : idx => chunk } : {} listener_arn = aws_lb_listener.http[0].arn - priority = 100 + priority = 100 + each.key tags = var.common_tags action { - type = "forward" target_group_arn = aws_lb_target_group.lambda[0].arn - } - - condition { - path_pattern { - values = ["/a/*", "/api/*", "/csrf/*", "/graphql", "/graphql/*"] - } - } -} - -resource "aws_lb_listener_rule" "backend_http_2" { - count = var.lambda_arn != null && !var.enable_https ? 1 : 0 - listener_arn = aws_lb_listener.http[0].arn - priority = 101 - tags = var.common_tags - - action { type = "forward" - target_group_arn = aws_lb_target_group.lambda[0].arn } condition { path_pattern { - values = ["/idx/*", "/integrations/*", "/sitemap", "/sitemap.xml", "/status/*"] + values = each.value } } } diff --git a/infrastructure/modules/cache/main.tf b/infrastructure/modules/cache/main.tf index 2b69012077..364cac64ba 100644 --- a/infrastructure/modules/cache/main.tf +++ b/infrastructure/modules/cache/main.tf @@ -43,7 +43,7 @@ resource "random_password" "redis_auth_token" { count = 1 length = 32 # Redis auth token has specific requirements for special characters. - override_special = "!&#$^<>-" + override_special = "!$^-" special = true } diff --git a/infrastructure/modules/parameters/main.tf b/infrastructure/modules/parameters/main.tf index 7befbf7bd4..9467b7d064 100644 --- a/infrastructure/modules/parameters/main.tf +++ b/infrastructure/modules/parameters/main.tf @@ -38,7 +38,7 @@ resource "aws_ssm_parameter" "django_algolia_write_api_key" { } resource "aws_ssm_parameter" "django_allowed_hosts" { - description = "Django allowed hosts for API Gateway - hostname only, no protocol (e.g., xxx.execute-api.region.amazonaws.com)." + description = "Django allowed hosts - hostname only, no protocol (e.g., nest.owasp.dev)." name = "/${var.project_name}/${var.environment}/DJANGO_ALLOWED_HOSTS" tags = var.common_tags type = "String" @@ -73,29 +73,6 @@ resource "aws_ssm_parameter" "django_configuration" { } } -resource "aws_ssm_parameter" "django_csrf_cookie_httponly" { - description = "Whether CSRF cookies should be HTTPOnly (false allows JavaScript to read for AJAX)." - name = "/${var.project_name}/${var.environment}/DJANGO_CSRF_COOKIE_HTTPONLY" - tags = var.common_tags - type = "String" - value = "False" - - lifecycle { - ignore_changes = [value] - } -} - -resource "aws_ssm_parameter" "django_csrf_cookie_samesite" { - description = "The SameSite attribute for CSRF cookies (Lax, Strict, or None for cross-domain)." - name = "/${var.project_name}/${var.environment}/DJANGO_CSRF_COOKIE_SAMESITE" - tags = var.common_tags - type = "String" - value = "Lax" - - lifecycle { - ignore_changes = [value] - } -} resource "aws_ssm_parameter" "django_db_host" { description = "The hostname of the database." @@ -181,17 +158,6 @@ resource "aws_ssm_parameter" "django_sentry_dsn" { } } -resource "aws_ssm_parameter" "django_session_cookie_samesite" { - description = "The SameSite attribute for session cookies (Lax, Strict, or None for cross-domain)." - name = "/${var.project_name}/${var.environment}/DJANGO_SESSION_COOKIE_SAMESITE" - tags = var.common_tags - type = "String" - value = "Lax" - - lifecycle { - ignore_changes = [value] - } -} resource "aws_ssm_parameter" "django_slack_bot_token" { description = "The bot token for the Slack integration." diff --git a/infrastructure/modules/parameters/outputs.tf b/infrastructure/modules/parameters/outputs.tf index f5483de189..0c65c630b8 100644 --- a/infrastructure/modules/parameters/outputs.tf +++ b/infrastructure/modules/parameters/outputs.tf @@ -1,27 +1,24 @@ output "django_ssm_parameter_arns" { description = "Map of environment variable names to the ARNs of all SSM parameters (Required by Django)." value = { - "DJANGO_ALGOLIA_APPLICATION_ID" = aws_ssm_parameter.django_algolia_application_id.arn - "DJANGO_ALGOLIA_WRITE_API_KEY" = aws_ssm_parameter.django_algolia_write_api_key.arn - "DJANGO_ALLOWED_HOSTS" = aws_ssm_parameter.django_allowed_hosts.arn - "DJANGO_ALLOWED_ORIGINS" = aws_ssm_parameter.django_allowed_origins.arn - "DJANGO_CONFIGURATION" = aws_ssm_parameter.django_configuration.arn - "DJANGO_CSRF_COOKIE_HTTPONLY" = aws_ssm_parameter.django_csrf_cookie_httponly.arn - "DJANGO_CSRF_COOKIE_SAMESITE" = aws_ssm_parameter.django_csrf_cookie_samesite.arn - "DJANGO_DB_HOST" = aws_ssm_parameter.django_db_host.arn - "DJANGO_DB_NAME" = aws_ssm_parameter.django_db_name.arn - "DJANGO_DB_PASSWORD" = var.db_password_arn - "DJANGO_DB_PORT" = aws_ssm_parameter.django_db_port.arn - "DJANGO_DB_USER" = aws_ssm_parameter.django_db_user.arn - "DJANGO_OPEN_AI_SECRET_KEY" = aws_ssm_parameter.django_open_ai_secret_key.arn - "DJANGO_REDIS_HOST" = aws_ssm_parameter.django_redis_host.arn - "DJANGO_REDIS_PASSWORD" = var.redis_password_arn - "DJANGO_SECRET_KEY" = aws_ssm_parameter.django_secret_key.arn - "DJANGO_SENTRY_DSN" = aws_ssm_parameter.django_sentry_dsn.arn - "DJANGO_SESSION_COOKIE_SAMESITE" = aws_ssm_parameter.django_session_cookie_samesite.arn - "DJANGO_SETTINGS_MODULE" = aws_ssm_parameter.django_settings_module.arn - "DJANGO_SLACK_BOT_TOKEN" = aws_ssm_parameter.django_slack_bot_token.arn - "DJANGO_SLACK_SIGNING_SECRET" = aws_ssm_parameter.django_slack_signing_secret.arn + "DJANGO_ALGOLIA_APPLICATION_ID" = aws_ssm_parameter.django_algolia_application_id.arn + "DJANGO_ALGOLIA_WRITE_API_KEY" = aws_ssm_parameter.django_algolia_write_api_key.arn + "DJANGO_ALLOWED_HOSTS" = aws_ssm_parameter.django_allowed_hosts.arn + "DJANGO_ALLOWED_ORIGINS" = aws_ssm_parameter.django_allowed_origins.arn + "DJANGO_CONFIGURATION" = aws_ssm_parameter.django_configuration.arn + "DJANGO_DB_HOST" = aws_ssm_parameter.django_db_host.arn + "DJANGO_DB_NAME" = aws_ssm_parameter.django_db_name.arn + "DJANGO_DB_PASSWORD" = var.db_password_arn + "DJANGO_DB_PORT" = aws_ssm_parameter.django_db_port.arn + "DJANGO_DB_USER" = aws_ssm_parameter.django_db_user.arn + "DJANGO_OPEN_AI_SECRET_KEY" = aws_ssm_parameter.django_open_ai_secret_key.arn + "DJANGO_REDIS_HOST" = aws_ssm_parameter.django_redis_host.arn + "DJANGO_REDIS_PASSWORD" = var.redis_password_arn + "DJANGO_SECRET_KEY" = aws_ssm_parameter.django_secret_key.arn + "DJANGO_SENTRY_DSN" = aws_ssm_parameter.django_sentry_dsn.arn + "DJANGO_SETTINGS_MODULE" = aws_ssm_parameter.django_settings_module.arn + "DJANGO_SLACK_BOT_TOKEN" = aws_ssm_parameter.django_slack_bot_token.arn + "DJANGO_SLACK_SIGNING_SECRET" = aws_ssm_parameter.django_slack_signing_secret.arn } } diff --git a/infrastructure/modules/parameters/variables.tf b/infrastructure/modules/parameters/variables.tf index ff5ca8e743..21f0866572 100644 --- a/infrastructure/modules/parameters/variables.tf +++ b/infrastructure/modules/parameters/variables.tf @@ -1,7 +1,6 @@ variable "allowed_hosts" { - description = "Django allowed hosts for API Gateway - hostname only, no protocol (e.g., xxx.execute-api.region.amazonaws.com)." + description = "Django allowed hosts - hostname only, no protocol (e.g., nest.owasp.dev)." type = string - default = "*" } variable "allowed_origins" { diff --git a/infrastructure/staging/main.tf b/infrastructure/staging/main.tf index 4f66828e67..19da47907c 100644 --- a/infrastructure/staging/main.tf +++ b/infrastructure/staging/main.tf @@ -128,6 +128,7 @@ module "networking" { module "parameters" { source = "../modules/parameters" + allowed_hosts = var.frontend_domain_name != null ? var.frontend_domain_name : module.alb.alb_dns_name allowed_origins = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.alb.alb_dns_name}" common_tags = local.common_tags db_host = module.database.db_proxy_endpoint From a10144808d131b251fbbe6d5a799b2e1a710f34e Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Wed, 7 Jan 2026 20:29:35 +0530 Subject: [PATCH 13/25] fix spell check --- cspell/custom-dict.txt | 1 + infrastructure/README.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cspell/custom-dict.txt b/cspell/custom-dict.txt index eeb2f778cd..6d4aab152b 100644 --- a/cspell/custom-dict.txt +++ b/cspell/custom-dict.txt @@ -73,6 +73,7 @@ gha graphiql gunicorn hackathon +hcl heroui hsl ics diff --git a/infrastructure/README.md b/infrastructure/README.md index 9674a1dd53..65293ff462 100644 --- a/infrastructure/README.md +++ b/infrastructure/README.md @@ -184,7 +184,7 @@ ECR Repositories are used to store images used by ECS (Frontend + Backend Tasks) aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 000000000000.dkr.ecr.us-east-2.amazonaws.com ``` -2. **Uplaod backend image to ECR**: +2. **Upload backend image to ECR**: - Build the backend image using the following command: @@ -210,7 +210,7 @@ ECR Repositories are used to store images used by ECS (Frontend + Backend Tasks) docker push 000000000000.dkr.ecr.us-east-2.amazonaws.com/owasp-nest-staging-backend:latest ``` -3. **Uplaod frontend image to ECR**: +3. **Upload frontend image to ECR**: - Build the frontend image using the following command: From 5460813b2ab36ae169413d7636eb6faa2f94582e Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Wed, 7 Jan 2026 21:31:52 +0530 Subject: [PATCH 14/25] Fix domain example --- infrastructure/staging/terraform.tfvars.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/staging/terraform.tfvars.example b/infrastructure/staging/terraform.tfvars.example index e15236d7e8..d0d1f94012 100644 --- a/infrastructure/staging/terraform.tfvars.example +++ b/infrastructure/staging/terraform.tfvars.example @@ -4,7 +4,7 @@ create_nat_gateway = true create_rds_proxy = false ecs_use_public_subnets = true environment = "staging" -frontend_domain_name = "https://nest.owasp.dev" +frontend_domain_name = "nest.owasp.dev" frontend_use_fargate_spot = true lambda_arn = null lambda_function_name = null From 7c9e9975363cd2fb44a331304b73559defca472b Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Wed, 7 Jan 2026 21:46:16 +0530 Subject: [PATCH 15/25] Update example --- infrastructure/staging/terraform.tfvars.example | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infrastructure/staging/terraform.tfvars.example b/infrastructure/staging/terraform.tfvars.example index d0d1f94012..db8a8963cd 100644 --- a/infrastructure/staging/terraform.tfvars.example +++ b/infrastructure/staging/terraform.tfvars.example @@ -1,8 +1,7 @@ availability_zones = ["us-east-2a", "us-east-2b", "us-east-2c"] aws_region = "us-east-2" -create_nat_gateway = true create_rds_proxy = false -ecs_use_public_subnets = true +ecs_use_public_subnets = false environment = "staging" frontend_domain_name = "nest.owasp.dev" frontend_use_fargate_spot = true From 49c4999bc87d03560edd85a805e41219288acd61 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 8 Jan 2026 18:58:30 +0530 Subject: [PATCH 16/25] Update code --- infrastructure/modules/alb/main.tf | 97 ++++++++++--------- infrastructure/modules/alb/variables.tf | 24 ++--- .../modules/ecs/modules/task/main.tf | 12 +-- infrastructure/modules/security/main.tf | 7 ++ infrastructure/staging/variables.tf | 48 ++++----- 5 files changed, 101 insertions(+), 87 deletions(-) diff --git a/infrastructure/modules/alb/main.tf b/infrastructure/modules/alb/main.tf index d274797851..3aebefabc0 100644 --- a/infrastructure/modules/alb/main.tf +++ b/infrastructure/modules/alb/main.tf @@ -34,6 +34,13 @@ locals { backend_path_chunks = chunklist(local.backend_paths, 5) } +check "https_requires_domain" { + assert { + condition = var.enable_https ? var.domain_name != null : true + error_message = "domain_name must be provided when enable_https is true." + } +} + data "aws_elb_service_account" "main" {} data "aws_iam_policy_document" "alb_logs" { @@ -63,6 +70,15 @@ resource "aws_acm_certificate" "main" { } } +resource "aws_lambda_permission" "alb" { + count = var.lambda_arn != null ? 1 : 0 + action = "lambda:InvokeFunction" + function_name = var.lambda_function_name + principal = "elasticloadbalancing.amazonaws.com" + source_arn = aws_lb_target_group.lambda[0].arn + statement_id = "AllowALBInvoke" +} + resource "aws_lb" "main" { depends_on = [aws_s3_bucket_policy.alb_logs] enable_deletion_protection = false @@ -128,6 +144,42 @@ resource "aws_lb_listener" "https" { } } +resource "aws_lb_listener_rule" "backend_http" { + for_each = var.lambda_arn != null && !var.enable_https ? { for idx, chunk in local.backend_path_chunks : idx => chunk } : {} + listener_arn = aws_lb_listener.http[0].arn + priority = 100 + each.key + tags = var.common_tags + + action { + target_group_arn = aws_lb_target_group.lambda[0].arn + type = "forward" + } + + condition { + path_pattern { + values = each.value + } + } +} + +resource "aws_lb_listener_rule" "backend_https" { + for_each = var.lambda_arn != null && var.enable_https ? { for idx, chunk in local.backend_path_chunks : idx => chunk } : {} + listener_arn = aws_lb_listener.https[0].arn + priority = 100 + each.key + tags = var.common_tags + + action { + target_group_arn = aws_lb_target_group.lambda[0].arn + type = "forward" + } + + condition { + path_pattern { + values = each.value + } + } +} + resource "aws_lb_target_group" "frontend" { deregistration_delay = 30 name = "${var.project_name}-${var.environment}-frontend-tg" @@ -167,51 +219,6 @@ resource "aws_lb_target_group_attachment" "lambda" { depends_on = [aws_lambda_permission.alb] } -resource "aws_lambda_permission" "alb" { - count = var.lambda_arn != null ? 1 : 0 - action = "lambda:InvokeFunction" - function_name = var.lambda_function_name - principal = "elasticloadbalancing.amazonaws.com" - source_arn = aws_lb_target_group.lambda[0].arn - statement_id = "AllowALBInvoke" -} - -resource "aws_lb_listener_rule" "backend_https" { - for_each = var.lambda_arn != null && var.enable_https ? { for idx, chunk in local.backend_path_chunks : idx => chunk } : {} - listener_arn = aws_lb_listener.https[0].arn - priority = 100 + each.key - tags = var.common_tags - - action { - target_group_arn = aws_lb_target_group.lambda[0].arn - type = "forward" - } - - condition { - path_pattern { - values = each.value - } - } -} - -resource "aws_lb_listener_rule" "backend_http" { - for_each = var.lambda_arn != null && !var.enable_https ? { for idx, chunk in local.backend_path_chunks : idx => chunk } : {} - listener_arn = aws_lb_listener.http[0].arn - priority = 100 + each.key - tags = var.common_tags - - action { - target_group_arn = aws_lb_target_group.lambda[0].arn - type = "forward" - } - - condition { - path_pattern { - values = each.value - } - } -} - resource "aws_s3_bucket" "alb_logs" { # NOSONAR bucket = "${var.project_name}-${var.environment}-alb-logs-${random_id.suffix.hex}" tags = merge(var.common_tags, { diff --git a/infrastructure/modules/alb/variables.tf b/infrastructure/modules/alb/variables.tf index a7ad5b6d08..aa26b6a7a8 100644 --- a/infrastructure/modules/alb/variables.tf +++ b/infrastructure/modules/alb/variables.tf @@ -26,18 +26,6 @@ variable "environment" { type = string } -variable "lambda_arn" { - description = "The ARN of the Lambda function for backend routing (null to skip Lambda routing)." - type = string - default = null -} - -variable "lambda_function_name" { - description = "The name of the Lambda function for backend routing." - type = string - default = null -} - variable "frontend_health_check_path" { description = "The health check path for the frontend target group." type = string @@ -50,6 +38,18 @@ variable "frontend_port" { default = 3000 } +variable "lambda_arn" { + description = "The ARN of the Lambda function for backend routing (null to skip Lambda routing)." + type = string + default = null +} + +variable "lambda_function_name" { + description = "The name of the Lambda function for backend routing." + type = string + default = null +} + variable "log_retention_days" { description = "The number of days to retain ALB access logs." type = number diff --git a/infrastructure/modules/ecs/modules/task/main.tf b/infrastructure/modules/ecs/modules/task/main.tf index c66628822f..86fb600000 100644 --- a/infrastructure/modules/ecs/modules/task/main.tf +++ b/infrastructure/modules/ecs/modules/task/main.tf @@ -65,25 +65,25 @@ resource "aws_cloudwatch_event_rule" "task" { resource "aws_cloudwatch_event_target" "task" { count = var.schedule_expression != null ? 1 : 0 - rule = aws_cloudwatch_event_rule.task[0].name - target_id = "${var.project_name}-${var.environment}-${var.task_name}-target" arn = var.ecs_cluster_arn role_arn = var.event_bridge_role_arn + rule = aws_cloudwatch_event_rule.task[0].name + target_id = "${var.project_name}-${var.environment}-${var.task_name}-target" ecs_target { - task_definition_arn = aws_ecs_task_definition.task.arn launch_type = null + task_definition_arn = aws_ecs_task_definition.task.arn capacity_provider_strategy { + base = 0 capacity_provider = var.use_fargate_spot ? "FARGATE_SPOT" : "FARGATE" weight = 1 - base = 0 } network_configuration { - subnets = var.subnet_ids - security_groups = var.security_group_ids assign_public_ip = var.assign_public_ip + security_groups = var.security_group_ids + subnets = var.subnet_ids } } } diff --git a/infrastructure/modules/security/main.tf b/infrastructure/modules/security/main.tf index acc1fa8864..8ab9276e5b 100644 --- a/infrastructure/modules/security/main.tf +++ b/infrastructure/modules/security/main.tf @@ -9,6 +9,13 @@ terraform { } } +check "vpc_endpoint_rules_require_sg" { + assert { + condition = var.create_vpc_endpoint_rules ? var.vpc_endpoint_sg_id != null : true + error_message = "vpc_endpoint_sg_id must be provided when create_vpc_endpoint_rules is true." + } +} + resource "aws_security_group" "alb" { description = "Security group for Application Load Balancer" name = "${var.project_name}-${var.environment}-alb-sg" diff --git a/infrastructure/staging/variables.tf b/infrastructure/staging/variables.tf index a8b50cb5b0..faa4ff9005 100644 --- a/infrastructure/staging/variables.tf +++ b/infrastructure/staging/variables.tf @@ -1,33 +1,21 @@ -variable "aws_region" { - description = "The AWS region to deploy resources in." - type = string - default = "us-east-2" -} - variable "availability_zones" { description = "A list of availability zones for the VPC." type = list(string) default = ["us-east-2a", "us-east-2b", "us-east-2c"] } +variable "aws_region" { + description = "The AWS region to deploy resources in." + type = string + default = "us-east-2" +} + variable "create_nat_gateway" { description = "Whether to create a NAT Gateway for private subnet internet access." type = bool default = true } -variable "lambda_arn" { - description = "The ARN of the Zappa Lambda function for backend routing." - type = string - default = null -} - -variable "lambda_function_name" { - description = "The name of the Zappa Lambda function." - type = string - default = null -} - variable "create_rds_proxy" { description = "Whether to create an RDS proxy." type = bool @@ -147,6 +135,12 @@ variable "db_user" { default = "owasp_nest_db_user" } +variable "ecs_use_public_subnets" { + description = "Whether to run ECS tasks in public subnets (requires assign_public_ip)." + type = bool + default = true +} + variable "environment" { description = "The environment (e.g., staging, production)." type = string @@ -157,12 +151,6 @@ variable "environment" { } } -variable "ecs_use_public_subnets" { - description = "Whether to run ECS tasks in public subnets (requires assign_public_ip)." - type = bool - default = true -} - variable "fixtures_bucket_name" { description = "The name of the S3 bucket for fixtures." type = string @@ -205,6 +193,18 @@ variable "frontend_use_fargate_spot" { default = true } +variable "lambda_arn" { + description = "The ARN of the Zappa Lambda function for backend routing." + type = string + default = null +} + +variable "lambda_function_name" { + description = "The name of the Zappa Lambda function." + type = string + default = null +} + variable "private_subnet_cidrs" { description = "A list of CIDR blocks for the private subnets." type = list(string) From df7ff5cd1169728a9b5549d6fc439a7a5543e6ec Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 8 Jan 2026 19:09:22 +0530 Subject: [PATCH 17/25] chamge assert to precondition --- infrastructure/modules/alb/main.tf | 18 +++++++++++------- infrastructure/modules/security/main.tf | 14 +++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/infrastructure/modules/alb/main.tf b/infrastructure/modules/alb/main.tf index 3aebefabc0..c8d38ab24c 100644 --- a/infrastructure/modules/alb/main.tf +++ b/infrastructure/modules/alb/main.tf @@ -34,13 +34,6 @@ locals { backend_path_chunks = chunklist(local.backend_paths, 5) } -check "https_requires_domain" { - assert { - condition = var.enable_https ? var.domain_name != null : true - error_message = "domain_name must be provided when enable_https is true." - } -} - data "aws_elb_service_account" "main" {} data "aws_iam_policy_document" "alb_logs" { @@ -96,6 +89,17 @@ resource "aws_lb" "main" { enabled = true prefix = "alb" } + + lifecycle { + precondition { + condition = var.enable_https ? var.domain_name != null : true + error_message = "domain_name must be provided when enable_https is true." + } + precondition { + condition = var.lambda_arn != null ? var.lambda_function_name != null : true + error_message = "lambda_function_name must be provided when lambda_arn is set." + } + } } resource "aws_lb_listener" "http" { diff --git a/infrastructure/modules/security/main.tf b/infrastructure/modules/security/main.tf index 8ab9276e5b..7228bec2d5 100644 --- a/infrastructure/modules/security/main.tf +++ b/infrastructure/modules/security/main.tf @@ -9,13 +9,6 @@ terraform { } } -check "vpc_endpoint_rules_require_sg" { - assert { - condition = var.create_vpc_endpoint_rules ? var.vpc_endpoint_sg_id != null : true - error_message = "vpc_endpoint_sg_id must be provided when create_vpc_endpoint_rules is true." - } -} - resource "aws_security_group" "alb" { description = "Security group for Application Load Balancer" name = "${var.project_name}-${var.environment}-alb-sg" @@ -23,6 +16,13 @@ resource "aws_security_group" "alb" { Name = "${var.project_name}-${var.environment}-alb-sg" }) vpc_id = var.vpc_id + + lifecycle { + precondition { + condition = var.create_vpc_endpoint_rules ? var.vpc_endpoint_sg_id != null : true + error_message = "vpc_endpoint_sg_id must be provided when create_vpc_endpoint_rules is true." + } + } } resource "aws_security_group" "ecs" { From 813c70a3c2b9d113eafc42b050569d221d504c71 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 8 Jan 2026 19:21:28 +0530 Subject: [PATCH 18/25] drop invalid headers --- infrastructure/modules/alb/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/infrastructure/modules/alb/main.tf b/infrastructure/modules/alb/main.tf index c8d38ab24c..2d47e81609 100644 --- a/infrastructure/modules/alb/main.tf +++ b/infrastructure/modules/alb/main.tf @@ -74,6 +74,7 @@ resource "aws_lambda_permission" "alb" { resource "aws_lb" "main" { depends_on = [aws_s3_bucket_policy.alb_logs] + drop_invalid_header_fields = true enable_deletion_protection = false internal = false load_balancer_type = "application" From fb7e34f4cefa6f6f95d899cd95ac835527b0f436 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 8 Jan 2026 19:22:37 +0530 Subject: [PATCH 19/25] never use Fargate SPOT for migrate task --- infrastructure/modules/ecs/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/modules/ecs/main.tf b/infrastructure/modules/ecs/main.tf index 99cc2d049a..e53c939652 100644 --- a/infrastructure/modules/ecs/main.tf +++ b/infrastructure/modules/ecs/main.tf @@ -311,7 +311,7 @@ module "migrate_task" { security_group_ids = [var.ecs_sg_id] subnet_ids = var.subnet_ids task_name = "migrate" - use_fargate_spot = var.use_fargate_spot + use_fargate_spot = false } module "load_data_task" { From 25f184ca974033942ac3ebedb7c6eec6faa7aae0 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 8 Jan 2026 19:27:34 +0530 Subject: [PATCH 20/25] Delete frontend alb module --- .../modules/frontend/modules/alb/main.tf | 180 ------------------ .../modules/frontend/modules/alb/outputs.tf | 24 --- .../modules/frontend/modules/alb/variables.tf | 54 ------ 3 files changed, 258 deletions(-) delete mode 100644 infrastructure/modules/frontend/modules/alb/main.tf delete mode 100644 infrastructure/modules/frontend/modules/alb/outputs.tf delete mode 100644 infrastructure/modules/frontend/modules/alb/variables.tf diff --git a/infrastructure/modules/frontend/modules/alb/main.tf b/infrastructure/modules/frontend/modules/alb/main.tf deleted file mode 100644 index ca95de60db..0000000000 --- a/infrastructure/modules/frontend/modules/alb/main.tf +++ /dev/null @@ -1,180 +0,0 @@ -terraform { - required_version = "1.14.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "6.22.0" - } - random = { - source = "hashicorp/random" - version = "3.7.2" - } - } -} - -data "aws_elb_service_account" "main" {} - -data "aws_iam_policy_document" "alb_logs" { - statement { - actions = ["s3:PutObject"] - effect = "Allow" - resources = ["${aws_s3_bucket.alb_logs.arn}/*"] - sid = "AllowALBLogDelivery" - - principals { - identifiers = [data.aws_elb_service_account.main.arn] - type = "AWS" - } - } -} - -resource "aws_lb" "main" { - depends_on = [aws_s3_bucket_policy.alb_logs] - enable_deletion_protection = false - internal = false - load_balancer_type = "application" - name = "${var.project_name}-${var.environment}-frontend-alb" - security_groups = [var.alb_sg_id] - subnets = var.public_subnet_ids - tags = merge(var.common_tags, { - Name = "${var.project_name}-${var.environment}-frontend-alb" - }) - - access_logs { - bucket = aws_s3_bucket.alb_logs.id - enabled = true - prefix = "frontend-alb" - } -} - -resource "aws_lb_listener" "http" { - count = var.enable_https ? 0 : 1 - load_balancer_arn = aws_lb.main.arn - port = 80 - protocol = "HTTP" #NOSONAR - tags = var.common_tags - - default_action { - target_group_arn = aws_lb_target_group.main.arn - type = "forward" - } -} - -resource "aws_lb_listener" "http_redirect" { - count = var.enable_https ? 1 : 0 - load_balancer_arn = aws_lb.main.arn - port = 80 - protocol = "HTTP" #NOSONAR - tags = var.common_tags - - default_action { - type = "redirect" - - redirect { - port = "443" - protocol = "HTTPS" - status_code = "HTTP_301" - } - } -} - -resource "aws_lb_listener" "https" { - certificate_arn = var.acm_certificate_arn - count = var.enable_https ? 1 : 0 - load_balancer_arn = aws_lb.main.arn - port = 443 - protocol = "HTTPS" - ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" - tags = var.common_tags - - default_action { - target_group_arn = aws_lb_target_group.main.arn - type = "forward" - } -} - -resource "aws_lb_target_group" "main" { - deregistration_delay = 30 - name = "${var.project_name}-${var.environment}-frontend-tg" - port = 3000 - protocol = "HTTP" - tags = merge(var.common_tags, { - Name = "${var.project_name}-${var.environment}-frontend-tg" - }) - target_type = "ip" - vpc_id = var.vpc_id - - health_check { - enabled = true - healthy_threshold = 2 - interval = 30 - matcher = "200-299" - path = var.health_check_path - protocol = "HTTP" - timeout = 5 - unhealthy_threshold = 3 - } -} - -resource "aws_s3_bucket" "alb_logs" { # NOSONAR - bucket = "${var.project_name}-${var.environment}-alb-logs-${random_id.suffix.hex}" - tags = merge(var.common_tags, { - Name = "${var.project_name}-${var.environment}-alb-logs" - }) - - lifecycle { - prevent_destroy = true - } -} - -resource "aws_s3_bucket_lifecycle_configuration" "alb_logs" { - bucket = aws_s3_bucket.alb_logs.id - - rule { - id = "expire-logs" - status = "Enabled" - - abort_incomplete_multipart_upload { - days_after_initiation = 7 - } - expiration { - days = var.log_retention_days - } - } -} - -resource "aws_s3_bucket_policy" "alb_logs" { - bucket = aws_s3_bucket.alb_logs.id - policy = data.aws_iam_policy_document.alb_logs.json -} - -resource "aws_s3_bucket_public_access_block" "alb_logs" { - block_public_acls = true - block_public_policy = true - bucket = aws_s3_bucket.alb_logs.id - ignore_public_acls = true - restrict_public_buckets = true -} - -resource "aws_s3_bucket_server_side_encryption_configuration" "alb_logs" { - bucket = aws_s3_bucket.alb_logs.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -resource "aws_s3_bucket_versioning" "alb_logs" { - bucket = aws_s3_bucket.alb_logs.id - - versioning_configuration { - status = "Enabled" - } -} - -resource "random_id" "suffix" { - byte_length = 4 -} diff --git a/infrastructure/modules/frontend/modules/alb/outputs.tf b/infrastructure/modules/frontend/modules/alb/outputs.tf deleted file mode 100644 index 7ee948330b..0000000000 --- a/infrastructure/modules/frontend/modules/alb/outputs.tf +++ /dev/null @@ -1,24 +0,0 @@ -output "alb_arn" { - description = "The ARN of the ALB." - value = aws_lb.main.arn -} - -output "alb_dns_name" { - description = "The DNS name of the ALB." - value = aws_lb.main.dns_name -} - -output "alb_zone_id" { - description = "The zone ID of the ALB." - value = aws_lb.main.zone_id -} - -output "http_listener_arn" { - description = "The ARN of the HTTP listener." - value = var.enable_https ? aws_lb_listener.http_redirect[0].arn : aws_lb_listener.http[0].arn -} - -output "target_group_arn" { - description = "The ARN of the target group." - value = aws_lb_target_group.main.arn -} diff --git a/infrastructure/modules/frontend/modules/alb/variables.tf b/infrastructure/modules/frontend/modules/alb/variables.tf deleted file mode 100644 index ab5988ca70..0000000000 --- a/infrastructure/modules/frontend/modules/alb/variables.tf +++ /dev/null @@ -1,54 +0,0 @@ -variable "acm_certificate_arn" { - description = "The ACM certificate ARN for HTTPS." - type = string - default = null -} - -variable "alb_sg_id" { - description = "The security group ID for the ALB." - type = string -} - -variable "common_tags" { - description = "The common tags to apply to all resources." - type = map(string) - default = {} -} - -variable "enable_https" { - description = "Whether to enable the HTTPS listener." - type = bool - default = false -} - -variable "environment" { - description = "The environment name." - type = string -} - -variable "health_check_path" { - description = "The health check path." - type = string - default = "/" -} - -variable "log_retention_days" { - description = "The number of days to retain ALB access logs." - type = number - default = 90 -} - -variable "project_name" { - description = "The project name." - type = string -} - -variable "public_subnet_ids" { - description = "The list of public subnet IDs." - type = list(string) -} - -variable "vpc_id" { - description = "The VPC ID." - type = string -} From fed6fe0c7b451626d3d65acaf8069325e0b87df9 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 8 Jan 2026 19:52:27 +0530 Subject: [PATCH 21/25] remove create_nat_gateway flag --- infrastructure/modules/networking/main.tf | 13 ++++--------- infrastructure/modules/networking/variables.tf | 6 ------ infrastructure/staging/main.tf | 6 ++++-- infrastructure/staging/variables.tf | 6 ------ 4 files changed, 8 insertions(+), 23 deletions(-) diff --git a/infrastructure/modules/networking/main.tf b/infrastructure/modules/networking/main.tf index ad9bb08e36..63ff9d39ba 100644 --- a/infrastructure/modules/networking/main.tf +++ b/infrastructure/modules/networking/main.tf @@ -136,7 +136,6 @@ resource "aws_subnet" "private" { } resource "aws_eip" "nat" { - count = var.create_nat_gateway ? 1 : 0 depends_on = [aws_internet_gateway.main] domain = "vpc" tags = merge(var.common_tags, { @@ -145,8 +144,7 @@ resource "aws_eip" "nat" { } resource "aws_nat_gateway" "main" { - count = var.create_nat_gateway ? 1 : 0 - allocation_id = aws_eip.nat[0].id + allocation_id = aws_eip.nat.id depends_on = [aws_internet_gateway.main] subnet_id = aws_subnet.public[0].id tags = merge(var.common_tags, { @@ -166,12 +164,9 @@ resource "aws_route_table" "public" { } resource "aws_route_table" "private" { - dynamic "route" { - for_each = var.create_nat_gateway ? [1] : [] - content { - cidr_block = "0.0.0.0/0" - nat_gateway_id = aws_nat_gateway.main[0].id - } + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.main.id } tags = merge(var.common_tags, { Name = "${var.project_name}-${var.environment}-private-rt" diff --git a/infrastructure/modules/networking/variables.tf b/infrastructure/modules/networking/variables.tf index c5417dff81..eb5a8f64f4 100644 --- a/infrastructure/modules/networking/variables.tf +++ b/infrastructure/modules/networking/variables.tf @@ -14,12 +14,6 @@ variable "common_tags" { default = {} } -variable "create_nat_gateway" { - description = "Whether to create a NAT Gateway for private subnet internet access." - type = bool - default = true -} - variable "create_vpc_cloudwatch_logs_endpoint" { description = "Whether to create CloudWatch Logs VPC endpoint." type = bool diff --git a/infrastructure/staging/main.tf b/infrastructure/staging/main.tf index 19da47907c..1c402ecad2 100644 --- a/infrastructure/staging/main.tf +++ b/infrastructure/staging/main.tf @@ -111,7 +111,6 @@ module "networking" { aws_region = var.aws_region availability_zones = var.availability_zones common_tags = local.common_tags - create_nat_gateway = var.create_nat_gateway create_vpc_cloudwatch_logs_endpoint = var.create_vpc_cloudwatch_logs_endpoint create_vpc_ecr_api_endpoint = var.create_vpc_ecr_api_endpoint create_vpc_ecr_dkr_endpoint = var.create_vpc_ecr_dkr_endpoint @@ -128,7 +127,10 @@ module "networking" { module "parameters" { source = "../modules/parameters" - allowed_hosts = var.frontend_domain_name != null ? var.frontend_domain_name : module.alb.alb_dns_name + allowed_hosts = join(",", [ + var.frontend_domain_name != null ? var.frontend_domain_name : module.alb.alb_dns_name, + "zappa", + ]) allowed_origins = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.alb.alb_dns_name}" common_tags = local.common_tags db_host = module.database.db_proxy_endpoint diff --git a/infrastructure/staging/variables.tf b/infrastructure/staging/variables.tf index faa4ff9005..4866709969 100644 --- a/infrastructure/staging/variables.tf +++ b/infrastructure/staging/variables.tf @@ -10,12 +10,6 @@ variable "aws_region" { default = "us-east-2" } -variable "create_nat_gateway" { - description = "Whether to create a NAT Gateway for private subnet internet access." - type = bool - default = true -} - variable "create_rds_proxy" { description = "Whether to create an RDS proxy." type = bool From ddad92dfa43201b7db5bdf47dc892829961d1c1f Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 8 Jan 2026 20:12:07 +0530 Subject: [PATCH 22/25] rename use_fargate_spot to ecs_use_fargate_spot --- infrastructure/staging/main.tf | 2 +- infrastructure/staging/terraform.tfvars.example | 2 +- infrastructure/staging/variables.tf | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/infrastructure/staging/main.tf b/infrastructure/staging/main.tf index 1c402ecad2..566ec7fc0b 100644 --- a/infrastructure/staging/main.tf +++ b/infrastructure/staging/main.tf @@ -84,7 +84,7 @@ module "ecs" { fixtures_read_only_policy_arn = module.storage.fixtures_read_only_policy_arn project_name = var.project_name subnet_ids = var.ecs_use_public_subnets ? module.networking.public_subnet_ids : module.networking.private_subnet_ids - use_fargate_spot = var.use_fargate_spot + use_fargate_spot = var.ecs_use_fargate_spot } module "frontend" { diff --git a/infrastructure/staging/terraform.tfvars.example b/infrastructure/staging/terraform.tfvars.example index db8a8963cd..461bff1844 100644 --- a/infrastructure/staging/terraform.tfvars.example +++ b/infrastructure/staging/terraform.tfvars.example @@ -1,6 +1,7 @@ availability_zones = ["us-east-2a", "us-east-2b", "us-east-2c"] aws_region = "us-east-2" create_rds_proxy = false +ecs_use_fargate_spot = true ecs_use_public_subnets = false environment = "staging" frontend_domain_name = "nest.owasp.dev" @@ -8,4 +9,3 @@ frontend_use_fargate_spot = true lambda_arn = null lambda_function_name = null project_name = "owasp-nest" -use_fargate_spot = true diff --git a/infrastructure/staging/variables.tf b/infrastructure/staging/variables.tf index 4866709969..6cc7b0126b 100644 --- a/infrastructure/staging/variables.tf +++ b/infrastructure/staging/variables.tf @@ -129,6 +129,12 @@ variable "db_user" { default = "owasp_nest_db_user" } +variable "ecs_use_fargate_spot" { + description = "Whether to use Fargate Spot for backend ECS tasks." + type = bool + default = true +} + variable "ecs_use_public_subnets" { description = "Whether to run ECS tasks in public subnets (requires assign_public_ip)." type = bool @@ -271,12 +277,6 @@ variable "secret_recovery_window_in_days" { default = 7 } -variable "use_fargate_spot" { - description = "Whether to use Fargate Spot for backend ECS tasks." - type = bool - default = true -} - variable "vpc_cidr" { description = "The CIDR block for the VPC." type = string From 7e0ac7784b0b56ce69675cb81b7a6463c5c80389 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 8 Jan 2026 20:16:21 +0530 Subject: [PATCH 23/25] never use fargate spot for index and load data tasks --- infrastructure/modules/ecs/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infrastructure/modules/ecs/main.tf b/infrastructure/modules/ecs/main.tf index e53c939652..d1613bbb20 100644 --- a/infrastructure/modules/ecs/main.tf +++ b/infrastructure/modules/ecs/main.tf @@ -345,7 +345,7 @@ module "load_data_task" { subnet_ids = var.subnet_ids task_name = "load-data" task_role_arn = aws_iam_role.ecs_task_role.arn - use_fargate_spot = var.use_fargate_spot + use_fargate_spot = false } module "index_data_task" { @@ -366,5 +366,5 @@ module "index_data_task" { security_group_ids = [var.ecs_sg_id] subnet_ids = var.subnet_ids task_name = "index-data" - use_fargate_spot = var.use_fargate_spot + use_fargate_spot = false } From 8c3fd816a15dbeb5230831fda9fc54f2dd413542 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 8 Jan 2026 21:33:23 +0530 Subject: [PATCH 24/25] Update code --- infrastructure/staging/main.tf | 10 +++++----- infrastructure/staging/outputs.tf | 2 +- infrastructure/staging/terraform.tfvars.example | 2 +- infrastructure/staging/variables.tf | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/infrastructure/staging/main.tf b/infrastructure/staging/main.tf index 566ec7fc0b..48d47415a9 100644 --- a/infrastructure/staging/main.tf +++ b/infrastructure/staging/main.tf @@ -22,8 +22,8 @@ module "alb" { alb_sg_id = module.security.alb_sg_id common_tags = local.common_tags - domain_name = var.frontend_domain_name - enable_https = var.frontend_domain_name != null + domain_name = var.domain_name + enable_https = var.domain_name != null environment = var.environment frontend_health_check_path = "/" frontend_port = 3000 @@ -128,10 +128,10 @@ module "parameters" { source = "../modules/parameters" allowed_hosts = join(",", [ - var.frontend_domain_name != null ? var.frontend_domain_name : module.alb.alb_dns_name, + var.domain_name != null ? var.domain_name : module.alb.alb_dns_name, "zappa", ]) - allowed_origins = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.alb.alb_dns_name}" + allowed_origins = var.domain_name != null ? "https://${var.domain_name}" : "http://${module.alb.alb_dns_name}" common_tags = local.common_tags db_host = module.database.db_proxy_endpoint db_name = var.db_name @@ -139,7 +139,7 @@ module "parameters" { db_port = var.db_port db_user = var.db_user environment = var.environment - nextauth_url = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.alb.alb_dns_name}" + nextauth_url = var.domain_name != null ? "https://${var.domain_name}" : "http://${module.alb.alb_dns_name}" project_name = var.project_name redis_host = module.cache.redis_primary_endpoint redis_password_arn = module.cache.redis_password_arn diff --git a/infrastructure/staging/outputs.tf b/infrastructure/staging/outputs.tf index d372b17066..07d711b45e 100644 --- a/infrastructure/staging/outputs.tf +++ b/infrastructure/staging/outputs.tf @@ -25,7 +25,7 @@ output "frontend_ecr_repository_url" { output "frontend_url" { description = "The URL to access the frontend." - value = var.frontend_domain_name != null ? "https://${var.frontend_domain_name}" : "http://${module.alb.alb_dns_name}" + value = var.domain_name != null ? "https://${var.domain_name}" : "http://${module.alb.alb_dns_name}" } output "lambda_security_group_id" { diff --git a/infrastructure/staging/terraform.tfvars.example b/infrastructure/staging/terraform.tfvars.example index 461bff1844..615f09a28d 100644 --- a/infrastructure/staging/terraform.tfvars.example +++ b/infrastructure/staging/terraform.tfvars.example @@ -1,10 +1,10 @@ availability_zones = ["us-east-2a", "us-east-2b", "us-east-2c"] aws_region = "us-east-2" create_rds_proxy = false +domain_name = "nest.owasp.dev" ecs_use_fargate_spot = true ecs_use_public_subnets = false environment = "staging" -frontend_domain_name = "nest.owasp.dev" frontend_use_fargate_spot = true lambda_arn = null lambda_function_name = null diff --git a/infrastructure/staging/variables.tf b/infrastructure/staging/variables.tf index 6cc7b0126b..b02dce9c08 100644 --- a/infrastructure/staging/variables.tf +++ b/infrastructure/staging/variables.tf @@ -129,6 +129,12 @@ variable "db_user" { default = "owasp_nest_db_user" } +variable "domain_name" { + description = "The domain name for the site." + type = string + default = null +} + variable "ecs_use_fargate_spot" { description = "Whether to use Fargate Spot for backend ECS tasks." type = bool @@ -163,12 +169,6 @@ variable "frontend_desired_count" { default = 2 } -variable "frontend_domain_name" { - description = "The domain name for frontend. When set, HTTPS is auto-enabled via ACM." - type = string - default = null -} - variable "frontend_enable_auto_scaling" { description = "Whether to enable auto scaling for frontend." type = bool From fa5b512264d096e9bdefd99bff8c321ddc629bdf Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 8 Jan 2026 21:55:12 +0530 Subject: [PATCH 25/25] fix indentation --- infrastructure/staging/terraform.tfvars.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/staging/terraform.tfvars.example b/infrastructure/staging/terraform.tfvars.example index 615f09a28d..119721755c 100644 --- a/infrastructure/staging/terraform.tfvars.example +++ b/infrastructure/staging/terraform.tfvars.example @@ -1,7 +1,7 @@ availability_zones = ["us-east-2a", "us-east-2b", "us-east-2c"] aws_region = "us-east-2" create_rds_proxy = false -domain_name = "nest.owasp.dev" +domain_name = "nest.owasp.dev" ecs_use_fargate_spot = true ecs_use_public_subnets = false environment = "staging"