From 1f8ced633dd8f01fac31bfb9bb61213f75f902b0 Mon Sep 17 00:00:00 2001 From: oycyc Date: Fri, 15 Aug 2025 18:39:11 -0400 Subject: [PATCH 1/7] feat: native ECS blue green deployment --- main.tf | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++ variables.tf | 28 ++++++++ 2 files changed, 208 insertions(+) diff --git a/main.tf b/main.tf index 76bebf5..c7cd6a6 100644 --- a/main.tf +++ b/main.tf @@ -418,6 +418,23 @@ resource "aws_ecs_service" "ignore_changes_task_definition" { force_new_deployment = var.force_new_deployment enable_execute_command = var.exec_enabled + dynamic "deployment_configuration" { + for_each = var.deployment_configuration == null ? [] : [var.deployment_configuration] + content { + strategy = try(deployment_configuration.value.strategy, null) + bake_time_in_minutes = try(deployment_configuration.value.bake_time_in_minutes, null) + + dynamic "lifecycle_hook" { + for_each = try(deployment_configuration.value.lifecycle_hooks, []) + content { + hook_target_arn = lifecycle_hook.value.hook_target_arn + role_arn = lifecycle_hook.value.role_arn + lifecycle_stages = lifecycle_hook.value.lifecycle_stages + } + } + } + } + dynamic "capacity_provider_strategy" { for_each = var.capacity_provider_strategies content { @@ -467,6 +484,24 @@ resource "aws_ecs_service" "ignore_changes_task_definition" { content { dns_name = client_alias.value.dns_name port = client_alias.value.port + + dynamic "test_traffic_rules" { + for_each = try(client_alias.value.test_traffic_rules, []) + content { + dynamic "header" { + for_each = try([test_traffic_rules.value.header], []) + content { + name = header.value.name + dynamic "value" { + for_each = try([header.value.value], []) + content { + exact = value.value.exact + } + } + } + } + } + } } } dynamic "timeout" { @@ -514,6 +549,16 @@ resource "aws_ecs_service" "ignore_changes_task_definition" { container_port = load_balancer.value.container_port elb_name = lookup(load_balancer.value, "elb_name", null) target_group_arn = lookup(load_balancer.value, "target_group_arn", null) + + dynamic "advanced_configuration" { + for_each = try([load_balancer.value.advanced_configuration], []) + content { + alternate_target_group_arn = advanced_configuration.value.alternate_target_group_arn + production_listener_rule = advanced_configuration.value.production_listener_rule + role_arn = advanced_configuration.value.role_arn + test_listener_rule = try(advanced_configuration.value.test_listener_rule, null) + } + } } } @@ -572,6 +617,23 @@ resource "aws_ecs_service" "ignore_changes_task_definition_and_desired_count" { force_new_deployment = var.force_new_deployment enable_execute_command = var.exec_enabled + dynamic "deployment_configuration" { + for_each = var.deployment_configuration == null ? [] : [var.deployment_configuration] + content { + strategy = try(deployment_configuration.value.strategy, null) + bake_time_in_minutes = try(deployment_configuration.value.bake_time_in_minutes, null) + + dynamic "lifecycle_hook" { + for_each = try(deployment_configuration.value.lifecycle_hooks, []) + content { + hook_target_arn = lifecycle_hook.value.hook_target_arn + role_arn = lifecycle_hook.value.role_arn + lifecycle_stages = lifecycle_hook.value.lifecycle_stages + } + } + } + } + dynamic "capacity_provider_strategy" { for_each = var.capacity_provider_strategies content { @@ -621,6 +683,24 @@ resource "aws_ecs_service" "ignore_changes_task_definition_and_desired_count" { content { dns_name = client_alias.value.dns_name port = client_alias.value.port + + dynamic "test_traffic_rules" { + for_each = try(client_alias.value.test_traffic_rules, []) + content { + dynamic "header" { + for_each = try([test_traffic_rules.value.header], []) + content { + name = header.value.name + dynamic "value" { + for_each = try([header.value.value], []) + content { + exact = value.value.exact + } + } + } + } + } + } } } dynamic "timeout" { @@ -668,6 +748,16 @@ resource "aws_ecs_service" "ignore_changes_task_definition_and_desired_count" { container_port = load_balancer.value.container_port elb_name = lookup(load_balancer.value, "elb_name", null) target_group_arn = lookup(load_balancer.value, "target_group_arn", null) + + dynamic "advanced_configuration" { + for_each = try([load_balancer.value.advanced_configuration], []) + content { + alternate_target_group_arn = advanced_configuration.value.alternate_target_group_arn + production_listener_rule = advanced_configuration.value.production_listener_rule + role_arn = advanced_configuration.value.role_arn + test_listener_rule = try(advanced_configuration.value.test_listener_rule, null) + } + } } } @@ -726,6 +816,23 @@ resource "aws_ecs_service" "ignore_changes_desired_count" { force_new_deployment = var.force_new_deployment enable_execute_command = var.exec_enabled + dynamic "deployment_configuration" { + for_each = var.deployment_configuration == null ? [] : [var.deployment_configuration] + content { + strategy = try(deployment_configuration.value.strategy, null) + bake_time_in_minutes = try(deployment_configuration.value.bake_time_in_minutes, null) + + dynamic "lifecycle_hook" { + for_each = try(deployment_configuration.value.lifecycle_hooks, []) + content { + hook_target_arn = lifecycle_hook.value.hook_target_arn + role_arn = lifecycle_hook.value.role_arn + lifecycle_stages = lifecycle_hook.value.lifecycle_stages + } + } + } + } + dynamic "capacity_provider_strategy" { for_each = var.capacity_provider_strategies content { @@ -775,6 +882,24 @@ resource "aws_ecs_service" "ignore_changes_desired_count" { content { dns_name = client_alias.value.dns_name port = client_alias.value.port + + dynamic "test_traffic_rules" { + for_each = try(client_alias.value.test_traffic_rules, []) + content { + dynamic "header" { + for_each = try([test_traffic_rules.value.header], []) + content { + name = header.value.name + dynamic "value" { + for_each = try([header.value.value], []) + content { + exact = value.value.exact + } + } + } + } + } + } } } dynamic "timeout" { @@ -822,6 +947,16 @@ resource "aws_ecs_service" "ignore_changes_desired_count" { container_port = load_balancer.value.container_port elb_name = lookup(load_balancer.value, "elb_name", null) target_group_arn = lookup(load_balancer.value, "target_group_arn", null) + + dynamic "advanced_configuration" { + for_each = try([load_balancer.value.advanced_configuration], []) + content { + alternate_target_group_arn = advanced_configuration.value.alternate_target_group_arn + production_listener_rule = advanced_configuration.value.production_listener_rule + role_arn = advanced_configuration.value.role_arn + test_listener_rule = try(advanced_configuration.value.test_listener_rule, null) + } + } } } @@ -880,6 +1015,23 @@ resource "aws_ecs_service" "default" { force_new_deployment = var.force_new_deployment enable_execute_command = var.exec_enabled + dynamic "deployment_configuration" { + for_each = var.deployment_configuration == null ? [] : [var.deployment_configuration] + content { + strategy = try(deployment_configuration.value.strategy, null) + bake_time_in_minutes = try(deployment_configuration.value.bake_time_in_minutes, null) + + dynamic "lifecycle_hook" { + for_each = try(deployment_configuration.value.lifecycle_hooks, []) + content { + hook_target_arn = lifecycle_hook.value.hook_target_arn + role_arn = lifecycle_hook.value.role_arn + lifecycle_stages = lifecycle_hook.value.lifecycle_stages + } + } + } + } + dynamic "capacity_provider_strategy" { for_each = var.capacity_provider_strategies content { @@ -929,6 +1081,24 @@ resource "aws_ecs_service" "default" { content { dns_name = client_alias.value.dns_name port = client_alias.value.port + + dynamic "test_traffic_rules" { + for_each = try(client_alias.value.test_traffic_rules, []) + content { + dynamic "header" { + for_each = try([test_traffic_rules.value.header], []) + content { + name = header.value.name + dynamic "value" { + for_each = try([header.value.value], []) + content { + exact = value.value.exact + } + } + } + } + } + } } } dynamic "timeout" { @@ -976,6 +1146,16 @@ resource "aws_ecs_service" "default" { container_port = load_balancer.value.container_port elb_name = lookup(load_balancer.value, "elb_name", null) target_group_arn = lookup(load_balancer.value, "target_group_arn", null) + + dynamic "advanced_configuration" { + for_each = try([load_balancer.value.advanced_configuration], []) + content { + alternate_target_group_arn = advanced_configuration.value.alternate_target_group_arn + production_listener_rule = advanced_configuration.value.production_listener_rule + role_arn = advanced_configuration.value.role_arn + test_listener_rule = try(advanced_configuration.value.test_listener_rule, null) + } + } } } diff --git a/variables.tf b/variables.tf index a171e73..e129b0a 100644 --- a/variables.tf +++ b/variables.tf @@ -14,6 +14,12 @@ variable "ecs_load_balancers" { container_port = number elb_name = optional(string) target_group_arn = string + advanced_configuration = optional(object({ + alternate_target_group_arn = string + production_listener_rule = string + role_arn = string + test_listener_rule = optional(string) + }), null) })) description = "A list of load balancer config objects for the ECS service; see [ecs_service#load_balancer](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#load_balancer) docs" default = [] @@ -446,6 +452,14 @@ variable "service_connect_configurations" { client_alias = list(object({ dns_name = string port = number + test_traffic_rules = optional(list(object({ + header = optional(object({ + name = string + value = optional(object({ + exact = string + }), null) + }), null) + })), []) })) timeout = optional(list(object({ idle_timeout_seconds = optional(number, null) @@ -470,6 +484,20 @@ variable "service_connect_configurations" { default = [] } +variable "deployment_configuration" { + type = object({ + strategy = optional(string) + bake_time_in_minutes = optional(number) + lifecycle_hooks = optional(list(object({ + hook_target_arn = string + role_arn = string + lifecycle_stages = list(string) + })), []) + }) + description = "ECS deployment configuration, supports BLUE_GREEN strategy with lifecycle hooks. See aws_ecs_service deployment_configuration (v6.4.0+)." + default = null +} + variable "permissions_boundary" { type = string description = "A permissions boundary ARN to apply to the 3 roles that are created." From aa59070a24ead7fbefa299949710e5c438f4d028 Mon Sep 17 00:00:00 2001 From: oycyc Date: Fri, 15 Aug 2025 18:45:23 -0400 Subject: [PATCH 2/7] simplify optional dynamic --- main.tf | 56 +++++++++++++++------------------------------------- variables.tf | 8 ++++---- 2 files changed, 20 insertions(+), 44 deletions(-) diff --git a/main.tf b/main.tf index c7cd6a6..98f75c8 100644 --- a/main.tf +++ b/main.tf @@ -488,16 +488,10 @@ resource "aws_ecs_service" "ignore_changes_task_definition" { dynamic "test_traffic_rules" { for_each = try(client_alias.value.test_traffic_rules, []) content { - dynamic "header" { - for_each = try([test_traffic_rules.value.header], []) - content { - name = header.value.name - dynamic "value" { - for_each = try([header.value.value], []) - content { - exact = value.value.exact - } - } + header { + name = test_traffic_rules.value.header.name + value { + exact = test_traffic_rules.value.header.value.exact } } } @@ -687,16 +681,10 @@ resource "aws_ecs_service" "ignore_changes_task_definition_and_desired_count" { dynamic "test_traffic_rules" { for_each = try(client_alias.value.test_traffic_rules, []) content { - dynamic "header" { - for_each = try([test_traffic_rules.value.header], []) - content { - name = header.value.name - dynamic "value" { - for_each = try([header.value.value], []) - content { - exact = value.value.exact - } - } + header { + name = test_traffic_rules.value.header.name + value { + exact = test_traffic_rules.value.header.value.exact } } } @@ -886,16 +874,10 @@ resource "aws_ecs_service" "ignore_changes_desired_count" { dynamic "test_traffic_rules" { for_each = try(client_alias.value.test_traffic_rules, []) content { - dynamic "header" { - for_each = try([test_traffic_rules.value.header], []) - content { - name = header.value.name - dynamic "value" { - for_each = try([header.value.value], []) - content { - exact = value.value.exact - } - } + header { + name = test_traffic_rules.value.header.name + value { + exact = test_traffic_rules.value.header.value.exact } } } @@ -1085,16 +1067,10 @@ resource "aws_ecs_service" "default" { dynamic "test_traffic_rules" { for_each = try(client_alias.value.test_traffic_rules, []) content { - dynamic "header" { - for_each = try([test_traffic_rules.value.header], []) - content { - name = header.value.name - dynamic "value" { - for_each = try([header.value.value], []) - content { - exact = value.value.exact - } - } + header { + name = test_traffic_rules.value.header.name + value { + exact = test_traffic_rules.value.header.value.exact } } } diff --git a/variables.tf b/variables.tf index e129b0a..e297eae 100644 --- a/variables.tf +++ b/variables.tf @@ -453,12 +453,12 @@ variable "service_connect_configurations" { dns_name = string port = number test_traffic_rules = optional(list(object({ - header = optional(object({ + header = object({ name = string - value = optional(object({ + value = object({ exact = string - }), null) - }), null) + }) + }) })), []) })) timeout = optional(list(object({ From 08ee465584b3fede23a4b9e06c4b25d0a23eecb5 Mon Sep 17 00:00:00 2001 From: oycyc Date: Fri, 15 Aug 2025 18:49:05 -0400 Subject: [PATCH 3/7] tf fmt --- variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variables.tf b/variables.tf index e297eae..97107af 100644 --- a/variables.tf +++ b/variables.tf @@ -454,7 +454,7 @@ variable "service_connect_configurations" { port = number test_traffic_rules = optional(list(object({ header = object({ - name = string + name = string value = object({ exact = string }) From 2e788570fb262667124d70843355fbe402c4acd6 Mon Sep 17 00:00:00 2001 From: oycyc Date: Sat, 16 Aug 2025 07:25:43 -0400 Subject: [PATCH 4/7] update subnet module to fix tests --- examples/complete/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/complete/main.tf b/examples/complete/main.tf index d7ae6ff..604cd5b 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -17,7 +17,7 @@ module "vpc" { module "subnets" { source = "cloudposse/dynamic-subnets/aws" - version = "2.1.0" + version = "2.4.2" availability_zones = var.availability_zones vpc_id = module.vpc.vpc_id From 851d704de949dd666735f7b5cebede829e8a8218 Mon Sep 17 00:00:00 2001 From: oycyc Date: Sat, 16 Aug 2025 07:33:57 -0400 Subject: [PATCH 5/7] better var --- variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variables.tf b/variables.tf index 97107af..4e5d059 100644 --- a/variables.tf +++ b/variables.tf @@ -494,7 +494,7 @@ variable "deployment_configuration" { lifecycle_stages = list(string) })), []) }) - description = "ECS deployment configuration, supports BLUE_GREEN strategy with lifecycle hooks. See aws_ecs_service deployment_configuration (v6.4.0+)." + description = "ECS deployment configuration, supports BLUE_GREEN strategy with lifecycle hooks. See aws_ecs_service deployment_configuration at https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#deployment_configuration -- default of null which is the default ROLLING deployment strategy." default = null } From e1f951920e7e432eab8093da79a9660387ef902b Mon Sep 17 00:00:00 2001 From: oycyc Date: Sat, 16 Aug 2025 07:38:46 -0400 Subject: [PATCH 6/7] variables --- variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variables.tf b/variables.tf index 4e5d059..3b73e2c 100644 --- a/variables.tf +++ b/variables.tf @@ -494,7 +494,7 @@ variable "deployment_configuration" { lifecycle_stages = list(string) })), []) }) - description = "ECS deployment configuration, supports BLUE_GREEN strategy with lifecycle hooks. See aws_ecs_service deployment_configuration at https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#deployment_configuration -- default of null which is the default ROLLING deployment strategy." + description = "ECS deployment configuration, supports blue green deployments (`strategy = 'BLUE_GREEN'`) with lifecycle hooks. See aws_ecs_service deployment_configuration at https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#deployment_configuration - default of null which is the default ROLLING deployment strategy." default = null } From 6e19d3b2b1af969e33449b07cb8f8eb4adade2eb Mon Sep 17 00:00:00 2001 From: oycyc Date: Sat, 16 Aug 2025 07:57:32 -0400 Subject: [PATCH 7/7] fix null advanced_config logic --- main.tf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.tf b/main.tf index 98f75c8..d1ee1ec 100644 --- a/main.tf +++ b/main.tf @@ -545,7 +545,7 @@ resource "aws_ecs_service" "ignore_changes_task_definition" { target_group_arn = lookup(load_balancer.value, "target_group_arn", null) dynamic "advanced_configuration" { - for_each = try([load_balancer.value.advanced_configuration], []) + for_each = try(load_balancer.value.advanced_configuration, null) == null ? [] : [load_balancer.value.advanced_configuration] content { alternate_target_group_arn = advanced_configuration.value.alternate_target_group_arn production_listener_rule = advanced_configuration.value.production_listener_rule @@ -738,7 +738,7 @@ resource "aws_ecs_service" "ignore_changes_task_definition_and_desired_count" { target_group_arn = lookup(load_balancer.value, "target_group_arn", null) dynamic "advanced_configuration" { - for_each = try([load_balancer.value.advanced_configuration], []) + for_each = try(load_balancer.value.advanced_configuration, null) == null ? [] : [load_balancer.value.advanced_configuration] content { alternate_target_group_arn = advanced_configuration.value.alternate_target_group_arn production_listener_rule = advanced_configuration.value.production_listener_rule @@ -931,7 +931,7 @@ resource "aws_ecs_service" "ignore_changes_desired_count" { target_group_arn = lookup(load_balancer.value, "target_group_arn", null) dynamic "advanced_configuration" { - for_each = try([load_balancer.value.advanced_configuration], []) + for_each = try(load_balancer.value.advanced_configuration, null) == null ? [] : [load_balancer.value.advanced_configuration] content { alternate_target_group_arn = advanced_configuration.value.alternate_target_group_arn production_listener_rule = advanced_configuration.value.production_listener_rule @@ -1124,7 +1124,7 @@ resource "aws_ecs_service" "default" { target_group_arn = lookup(load_balancer.value, "target_group_arn", null) dynamic "advanced_configuration" { - for_each = try([load_balancer.value.advanced_configuration], []) + for_each = try(load_balancer.value.advanced_configuration, null) == null ? [] : [load_balancer.value.advanced_configuration] content { alternate_target_group_arn = advanced_configuration.value.alternate_target_group_arn production_listener_rule = advanced_configuration.value.production_listener_rule