From 91bf3cc62d79a338c2b47f4a9cf848c2b4494be2 Mon Sep 17 00:00:00 2001 From: "amazon-q-developer[bot]" <208079219+amazon-q-developer[bot]@users.noreply.github.com> Date: Thu, 29 May 2025 23:53:31 +0000 Subject: [PATCH 1/3] feat: Add path-based routing support for ALB listeners Implements configurable path-based and host-based routing rules for both external and internal Application Load Balancers. Allows multiple services to run on different URL paths within the same ECS cluster. --- README.md | 36 ++++++++- _outputs.tf | 10 +++ _variables.tf | 22 +++++ alb-listener-rules.tf | 139 ++++++++++++++++++++++++++++++++ alb-listener-rules_test.go | 59 ++++++++++++++ example/ecs-app-path-routing.tf | 77 ++++++++++++++++++ 6 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 alb-listener-rules.tf create mode 100644 alb-listener-rules_test.go create mode 100644 example/ecs-app-path-routing.tf diff --git a/README.md b/README.md index 24ca8fd..7fb96b1 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,44 @@ module "ecs_apps" { on_demand_percentage = 0 asg_min = 1 asg_max = 4 - asg_target_capacity = 50 + asg_target_capacity = 50 + + # Path-based routing example + alb_listener_rules = [ + { + path_pattern = "/api/service1/*" + target_group_arn = "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/service1/abcdef1234567890" + priority = 100 + }, + { + path_pattern = "/api/service2/*" + target_group_arn = "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/service2/abcdef1234567890" + priority = 110 + host_header = "example.com" # Optional: Add host-based routing + } + ] + + # For internal ALB (if enabled) + alb_internal_listener_rules = [ + { + path_pattern = "/internal-api/*" + target_group_arn = "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/internal-service/abcdef1234567890" + priority = 100 + } + ] } ``` +### Path-Based Routing + +This module now supports path-based routing for both external and internal ALBs. This allows you to run multiple services in a single ECS cluster with different URL paths. The default action of the ALB listener will still forward to the default target group, but you can define rules to forward specific paths to different target groups. + +To use path-based routing, define the `alb_listener_rules` variable with a list of objects containing: +- `path_pattern`: The path pattern to match (e.g., "/api/*") +- `target_group_arn`: The ARN of the target group to forward requests to +- `priority`: The priority of the rule (lower numbers are evaluated first) +- `host_header`: (Optional) If specified, the rule will only match requests with this host header + ## Requirements diff --git a/_outputs.tf b/_outputs.tf index a31a832..d9d6596 100644 --- a/_outputs.tf +++ b/_outputs.tf @@ -86,6 +86,16 @@ output "alb_internal_listener_test_traffic_arn" { value = try(aws_lb_listener.ecs_test_https_internal.*.arn, "") } +output "alb_path_based_routing_rules" { + value = try(aws_lb_listener_rule.path_based_routing.*.id, []) + description = "IDs of the path-based routing rules for the external ALB" +} + +output "alb_internal_path_based_routing_rules" { + value = try(aws_lb_listener_rule.path_based_routing_internal.*.id, []) + description = "IDs of the path-based routing rules for the internal ALB" +} + output "ecs_nodes_secgrp_id" { value = aws_security_group.ecs_nodes.id } diff --git a/_variables.tf b/_variables.tf index 2d19670..946d8c6 100644 --- a/_variables.tf +++ b/_variables.tf @@ -389,4 +389,26 @@ variable "idle_timeout" { type = number default = 400 description = "IDLE time for ALB on seconds." +} + +variable "alb_listener_rules" { + description = "A list of maps describing the Listener Rules for path-based routing on the external ALB" + type = list(object({ + path_pattern = string + target_group_arn = string + priority = number + host_header = optional(string) + })) + default = [] +} + +variable "alb_internal_listener_rules" { + description = "A list of maps describing the Listener Rules for path-based routing on the internal ALB" + type = list(object({ + path_pattern = string + target_group_arn = string + priority = number + host_header = optional(string) + })) + default = [] } \ No newline at end of file diff --git a/alb-listener-rules.tf b/alb-listener-rules.tf new file mode 100644 index 0000000..e10a15d --- /dev/null +++ b/alb-listener-rules.tf @@ -0,0 +1,139 @@ +resource "aws_lb_listener_rule" "path_based_routing" { + count = var.alb ? length(var.alb_listener_rules) : 0 + + listener_arn = aws_lb_listener.ecs_https[0].arn + priority = var.alb_listener_rules[count.index].priority + + action { + type = "forward" + target_group_arn = var.alb_listener_rules[count.index].target_group_arn + } + + condition { + path_pattern { + values = [var.alb_listener_rules[count.index].path_pattern] + } + } + + dynamic "condition" { + for_each = var.alb_listener_rules[count.index].host_header != null ? [1] : [] + + content { + host_header { + values = [var.alb_listener_rules[count.index].host_header] + } + } + } + + tags = merge( + var.tags, + { + "Terraform" = true + }, + ) +} + +resource "aws_lb_listener_rule" "path_based_routing_test" { + count = var.alb && var.alb_test_listener ? length(var.alb_listener_rules) : 0 + + listener_arn = aws_lb_listener.ecs_test_https[0].arn + priority = var.alb_listener_rules[count.index].priority + + action { + type = "forward" + target_group_arn = var.alb_listener_rules[count.index].target_group_arn + } + + condition { + path_pattern { + values = [var.alb_listener_rules[count.index].path_pattern] + } + } + + dynamic "condition" { + for_each = var.alb_listener_rules[count.index].host_header != null ? [1] : [] + + content { + host_header { + values = [var.alb_listener_rules[count.index].host_header] + } + } + } + + tags = merge( + var.tags, + { + "Terraform" = true + }, + ) +} + +resource "aws_lb_listener_rule" "path_based_routing_internal" { + count = var.alb_internal ? length(var.alb_internal_listener_rules) : 0 + + listener_arn = aws_lb_listener.ecs_https_internal[0].arn + priority = var.alb_internal_listener_rules[count.index].priority + + action { + type = "forward" + target_group_arn = var.alb_internal_listener_rules[count.index].target_group_arn + } + + condition { + path_pattern { + values = [var.alb_internal_listener_rules[count.index].path_pattern] + } + } + + dynamic "condition" { + for_each = var.alb_internal_listener_rules[count.index].host_header != null ? [1] : [] + + content { + host_header { + values = [var.alb_internal_listener_rules[count.index].host_header] + } + } + } + + tags = merge( + var.tags, + { + "Terraform" = true + }, + ) +} + +resource "aws_lb_listener_rule" "path_based_routing_internal_test" { + count = var.alb_internal && var.alb_test_listener ? length(var.alb_internal_listener_rules) : 0 + + listener_arn = aws_lb_listener.ecs_test_https_internal[0].arn + priority = var.alb_internal_listener_rules[count.index].priority + + action { + type = "forward" + target_group_arn = var.alb_internal_listener_rules[count.index].target_group_arn + } + + condition { + path_pattern { + values = [var.alb_internal_listener_rules[count.index].path_pattern] + } + } + + dynamic "condition" { + for_each = var.alb_internal_listener_rules[count.index].host_header != null ? [1] : [] + + content { + host_header { + values = [var.alb_internal_listener_rules[count.index].host_header] + } + } + } + + tags = merge( + var.tags, + { + "Terraform" = true + }, + ) +} \ No newline at end of file diff --git a/alb-listener-rules_test.go b/alb-listener-rules_test.go new file mode 100644 index 0000000..622b811 --- /dev/null +++ b/alb-listener-rules_test.go @@ -0,0 +1,59 @@ +package test + +import ( + "testing" + + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" +) + +func TestAlbListenerRules(t *testing.T) { + t.Parallel() + + terraformOptions := &terraform.Options{ + // The path to where our Terraform code is located + TerraformDir: ".", + + // Variables to pass to our Terraform code using -var options + Vars: map[string]interface{}{ + "name": "test-cluster", + "vpc_id": "vpc-12345678", + "private_subnet_ids": []string{"subnet-1", "subnet-2"}, + "public_subnet_ids": []string{"subnet-3", "subnet-4"}, + "secure_subnet_ids": []string{"subnet-5", "subnet-6"}, + "certificate_arn": "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012", + "alb": true, + "alb_listener_rules": []map[string]interface{}{ + { + "path_pattern": "/api/service1/*", + "target_group_arn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/service1/abcdef1234567890", + "priority": 100, + }, + { + "path_pattern": "/api/service2/*", + "target_group_arn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/service2/abcdef1234567890", + "priority": 110, + "host_header": "example.com", + }, + }, + }, + + // Variables to pass to our Terraform code using -var-file options + VarFiles: []string{}, + + // Disable colors in Terraform commands so its easier to parse stdout/stderr + NoColor: true, + } + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer terraform.Destroy(t, terraformOptions) + + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors + terraform.InitAndApply(t, terraformOptions) + + // Run `terraform output` to get the values of output variables + albPathBasedRoutingRules := terraform.OutputList(t, terraformOptions, "alb_path_based_routing_rules") + + // Verify we have the correct number of rules + assert.Equal(t, 2, len(albPathBasedRoutingRules)) +} \ No newline at end of file diff --git a/example/ecs-app-path-routing.tf b/example/ecs-app-path-routing.tf new file mode 100644 index 0000000..f8ddbc5 --- /dev/null +++ b/example/ecs-app-path-routing.tf @@ -0,0 +1,77 @@ +# Example of using path-based routing with multiple services + +# First service - WordPress +module "ecs_app_wordpress_01" { + source = "git::https://github.com/DNXLabs/terraform-aws-ecs-app.git?ref=1.5.0" + vpc_id = local.workspace["vpc_id"] + cluster_name = module.ecs_apps.ecs_name + service_role_arn = module.ecs_apps.ecs_service_iam_role_arn + task_role_arn = module.ecs_apps.ecs_task_iam_role_arn + alb_listener_https_arn = element(module.ecs_apps.alb_listener_https_arn, 0) + alb_dns_name = element(module.ecs_apps.alb_dns_name, 0) + name = "wordpress-01" + image = "nginxdemos/hello:latest" + container_port = 80 + hostname = "wp01.labs.dnx.host" + hostname_blue = "wp01-blue.labs.dnx.host" + hostname_origin = "wp01-origin.labs.dnx.host" + hosted_zone = "labs.dnx.host" + certificate_arn = local.workspace["cf_certificate_arn"] + healthcheck_path = "/readme.html" + service_health_check_grace_period_seconds = 120 +} + +# Second service - API +module "ecs_app_api" { + source = "git::https://github.com/DNXLabs/terraform-aws-ecs-app.git?ref=1.5.0" + vpc_id = local.workspace["vpc_id"] + cluster_name = module.ecs_apps.ecs_name + service_role_arn = module.ecs_apps.ecs_service_iam_role_arn + task_role_arn = module.ecs_apps.ecs_task_iam_role_arn + alb_listener_https_arn = element(module.ecs_apps.alb_listener_https_arn, 0) + alb_dns_name = element(module.ecs_apps.alb_dns_name, 0) + name = "api" + image = "nginxdemos/hello:latest" + container_port = 80 + hostname = "api.labs.dnx.host" + hostname_blue = "api-blue.labs.dnx.host" + hostname_origin = "api-origin.labs.dnx.host" + hosted_zone = "labs.dnx.host" + certificate_arn = local.workspace["cf_certificate_arn"] + healthcheck_path = "/health" + service_health_check_grace_period_seconds = 120 +} + +# Configure path-based routing in the ECS cluster module +locals { + path_based_routing_rules = [ + { + path_pattern = "/wordpress/*" + target_group_arn = module.ecs_app_wordpress_01.target_group_arn + priority = 100 + }, + { + path_pattern = "/api/*" + target_group_arn = module.ecs_app_api.target_group_arn + priority = 110 + } + ] +} + +# Update the ECS cluster module to use path-based routing +module "ecs_apps" { + source = "git::https://github.com/DNXLabs/terraform-aws-ecs.git?ref=0.2.0" + name = local.workspace["cluster_name"] + instance_type_1 = "t3.large" + instance_type_2 = "t2.large" + instance_type_3 = "m2.xlarge" + vpc_id = local.workspace["vpc_id"] + private_subnet_ids = [split(",", local.workspace["private_subnet_ids"])] + public_subnet_ids = [split(",", local.workspace["public_subnet_ids"])] + secure_subnet_ids = [split(",", local.workspace["secure_subnet_ids"])] + certificate_arn = local.workspace["alb_certificate_arn"] + on_demand_percentage = 0 + + # Path-based routing configuration + alb_listener_rules = local.path_based_routing_rules +} \ No newline at end of file From af27ba18aa5dccb7e6ba2b754ba4f8ad428e2552 Mon Sep 17 00:00:00 2001 From: "amazon-q-developer[bot]" Date: Thu, 29 May 2025 23:53:54 +0000 Subject: [PATCH 2/3] terraform-docs: automated update action --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7fb96b1..684aaa2 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,9 @@ To use path-based routing, define the `alb_listener_rules` variable with a list | alb\_enable\_deletion\_protection | Enable deletion protection for ALBs | `bool` | `false` | no | | alb\_http\_listener | Whether to enable HTTP listeners | `bool` | `true` | no | | alb\_internal | Deploys a second internal ALB for private APIs. | `bool` | `false` | no | +| alb\_internal\_listener\_rules | A list of maps describing the Listener Rules for path-based routing on the internal ALB |
list(object({
path_pattern = string
target_group_arn = string
priority = number
host_header = optional(string)
}))
| `[]` | no | | alb\_internal\_ssl\_policy | The name of the SSL Policy for the listener. Required if protocol is HTTPS or TLS. | `string` | `"ELBSecurityPolicy-TLS-1-2-Ext-2018-06"` | no | +| alb\_listener\_rules | A list of maps describing the Listener Rules for path-based routing on the external ALB |
list(object({
path_pattern = string
target_group_arn = string
priority = number
host_header = optional(string)
}))
| `[]` | no | | alb\_only | Whether to deploy only an alb and no cloudFront or not with the cluster. | `bool` | `false` | no | | alb\_sg\_allow\_alb\_test\_listener | Whether to allow world access to the test listeners | `bool` | `true` | no | | alb\_sg\_allow\_egress\_https\_world | Whether to allow ALB to access HTTPS endpoints - needed when using OIDC authentication | `bool` | `true` | no | @@ -180,10 +182,12 @@ To use path-based routing, define the `alb_listener_rules` variable with a list | alb\_internal\_id | n/a | | alb\_internal\_listener\_https\_arn | n/a | | alb\_internal\_listener\_test\_traffic\_arn | n/a | +| alb\_internal\_path\_based\_routing\_rules | IDs of the path-based routing rules for the internal ALB | | alb\_internal\_secgrp\_id | n/a | | alb\_internal\_zone\_id | n/a | | alb\_listener\_https\_arn | n/a | | alb\_listener\_test\_traffic\_arn | n/a | +| alb\_path\_based\_routing\_rules | IDs of the path-based routing rules for the external ALB | | alb\_secgrp\_id | n/a | | alb\_zone\_id | n/a | | ecs\_arn | n/a | From fd7434b40d4b555b87760eab2b4acbfb62b944a8 Mon Sep 17 00:00:00 2001 From: "amazon-q-developer[bot]" <208079219+amazon-q-developer[bot]@users.noreply.github.com> Date: Thu, 29 May 2025 23:57:06 +0000 Subject: [PATCH 3/3] style: align terraform variable assignments and clean whitespace Standardizes formatting of value assignments in Terraform files to improve readability. --- _outputs.tf | 4 ++-- alb-listener-rules.tf | 8 ++++---- example/ecs-app-path-routing.tf | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/_outputs.tf b/_outputs.tf index d9d6596..f497aa9 100644 --- a/_outputs.tf +++ b/_outputs.tf @@ -87,12 +87,12 @@ output "alb_internal_listener_test_traffic_arn" { } output "alb_path_based_routing_rules" { - value = try(aws_lb_listener_rule.path_based_routing.*.id, []) + value = try(aws_lb_listener_rule.path_based_routing.*.id, []) description = "IDs of the path-based routing rules for the external ALB" } output "alb_internal_path_based_routing_rules" { - value = try(aws_lb_listener_rule.path_based_routing_internal.*.id, []) + value = try(aws_lb_listener_rule.path_based_routing_internal.*.id, []) description = "IDs of the path-based routing rules for the internal ALB" } diff --git a/alb-listener-rules.tf b/alb-listener-rules.tf index e10a15d..4c24f99 100644 --- a/alb-listener-rules.tf +++ b/alb-listener-rules.tf @@ -17,7 +17,7 @@ resource "aws_lb_listener_rule" "path_based_routing" { dynamic "condition" { for_each = var.alb_listener_rules[count.index].host_header != null ? [1] : [] - + content { host_header { values = [var.alb_listener_rules[count.index].host_header] @@ -52,7 +52,7 @@ resource "aws_lb_listener_rule" "path_based_routing_test" { dynamic "condition" { for_each = var.alb_listener_rules[count.index].host_header != null ? [1] : [] - + content { host_header { values = [var.alb_listener_rules[count.index].host_header] @@ -87,7 +87,7 @@ resource "aws_lb_listener_rule" "path_based_routing_internal" { dynamic "condition" { for_each = var.alb_internal_listener_rules[count.index].host_header != null ? [1] : [] - + content { host_header { values = [var.alb_internal_listener_rules[count.index].host_header] @@ -122,7 +122,7 @@ resource "aws_lb_listener_rule" "path_based_routing_internal_test" { dynamic "condition" { for_each = var.alb_internal_listener_rules[count.index].host_header != null ? [1] : [] - + content { host_header { values = [var.alb_internal_listener_rules[count.index].host_header] diff --git a/example/ecs-app-path-routing.tf b/example/ecs-app-path-routing.tf index f8ddbc5..d1a7897 100644 --- a/example/ecs-app-path-routing.tf +++ b/example/ecs-app-path-routing.tf @@ -16,9 +16,9 @@ module "ecs_app_wordpress_01" { hostname_blue = "wp01-blue.labs.dnx.host" hostname_origin = "wp01-origin.labs.dnx.host" hosted_zone = "labs.dnx.host" - certificate_arn = local.workspace["cf_certificate_arn"] - healthcheck_path = "/readme.html" - service_health_check_grace_period_seconds = 120 + certificate_arn = local.workspace["cf_certificate_arn"] + healthcheck_path = "/readme.html" + service_health_check_grace_period_seconds = 120 } # Second service - API @@ -37,9 +37,9 @@ module "ecs_app_api" { hostname_blue = "api-blue.labs.dnx.host" hostname_origin = "api-origin.labs.dnx.host" hosted_zone = "labs.dnx.host" - certificate_arn = local.workspace["cf_certificate_arn"] - healthcheck_path = "/health" - service_health_check_grace_period_seconds = 120 + certificate_arn = local.workspace["cf_certificate_arn"] + healthcheck_path = "/health" + service_health_check_grace_period_seconds = 120 } # Configure path-based routing in the ECS cluster module