Skip to content

Commit ab80776

Browse files
Benbentwogithub-actions[bot]coderabbitai[bot]
authored
improvement CICD Perma drift resolution, feature enhancements to control over ECS Service deployment (#77)
* update to avoid permadrift * Update src/variables.tf Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * code suggestion improvements * cleanup 1 * Update to improve functionality and permadrift * <output Update changelog to include this PR.</output> * Update src/variables.tf Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * cleanup from pr comments * update ecs logic to fix tests * terraform fmt -recursive * try again * Coder rabbit suggestions --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 9c51420 commit ab80776

File tree

8 files changed

+439
-244
lines changed

8 files changed

+439
-244
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
## PR [#77](https://github.com/cloudposse-terraform-components/aws-ecs-service/pull/77)
2+
3+
### Added
4+
5+
- Added `additional_lb_target_groups` variable to support registering multiple container ports to load balancer target groups
6+
- Enables sidecar containers (e.g., stunnel, envoy) to be registered to separate target groups
7+
- Each entry requires a unique `target_group_arn` to prevent all additional ports from being wired to the same target group
8+
- Includes validation to ensure valid target group ARN format
9+
- Added `additional_security_groups` variable to attach additional security groups to the ECS service
10+
- Added `security_group_id` field to `custom_security_group_rules` to allow rules to target different security groups
11+
12+
### Changed
13+
14+
- Updated `task_template` output to use S3 task definition when available via merge strategy
15+
- When `task-definition.json` exists in S3 (created by CI/CD), it takes precedence
16+
- Falls back to Terraform-created task definition when S3 version doesn't exist
17+
- Eliminates Terraform drift when CI/CD manages task definitions
18+
- Fixed deprecation warnings:
19+
- Replaced `inline_policy` in `aws_iam_role.github_actions` with separate `aws_iam_role_policy` resource
20+
- Replaced deprecated `aws_s3_bucket_object` with `aws_s3_object`
21+
- Updated SSM parameter key format to include attributes in the path for better organization
22+
123
## PR [#1008](https://github.com/cloudposse/terraform-aws-components/pull/1008)
224

325
### Possible Breaking Change

src/README.md

Lines changed: 301 additions & 214 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/github-actions-iam-policy.tf

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,18 @@ data "aws_iam_policy_document" "github_actions_iam_ecspresso_policy" {
103103
"ecs:RunTask",
104104
]
105105
resources = [
106-
format("arn:%s:ecs:%s:%s:task-definition/%s:*", local.aws_partition, var.region, local.account_id, join("", module.ecs_alb_service_task[*].task_definition_family)),
106+
format(
107+
"arn:%s:ecs:%s:%s:task-definition/%s:*",
108+
local.aws_partition,
109+
var.region,
110+
local.account_id,
111+
try(one(module.ecs_alb_service_task[*].task_definition_family), null) != null
112+
? try(one(module.ecs_alb_service_task[*].task_definition_family), null)
113+
: (try(one(data.aws_ecs_task_definition.created_task[*].family), null) != null
114+
? one(data.aws_ecs_task_definition.created_task[*].family)
115+
: module.this.id
116+
)
117+
),
107118
]
108119
}
109120

src/github-actions-iam-role.mixin.tf

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ resource "aws_iam_role" "github_actions" {
5454
count = local.github_actions_iam_role_enabled ? 1 : 0
5555
name = module.gha_role_name.id
5656
assume_role_policy = module.gha_assume_role.github_assume_role_policy
57+
}
5758

58-
inline_policy {
59-
name = module.gha_role_name.id
60-
policy = local.github_actions_iam_policy
61-
}
59+
resource "aws_iam_role_policy" "github_actions" {
60+
count = local.github_actions_iam_role_enabled ? 1 : 0
61+
name = module.gha_role_name.id
62+
role = aws_iam_role.github_actions[0].id
63+
policy = local.github_actions_iam_policy
6264
}
6365

6466
output "github_actions_iam_role_arn" {

src/main.tf

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ locals {
1010

1111
assign_public_ip = try(local.task["assign_public_ip"], false)
1212

13-
container_definition = concat([
14-
for container in module.container_definition :
15-
container.json_map_object
13+
container_definition = concat(
14+
[
15+
16+
for container in module.container_definition :
17+
container.json_map_object
1618
],
1719
[
1820
for container in module.datadog_container_definition :
@@ -129,16 +131,16 @@ locals {
129131
local.container_aliases[item.name] => { container_definition = item }
130132
}
131133

132-
containers_priority_terraform = {
134+
containers_priority_terraform = local.enabled ? {
133135
for name, settings in var.containers :
134136
name => merge(local.container_chamber[name], lookup(local.container_s3, name, {}), settings, )
135137
if local.enabled
136-
}
137-
containers_priority_s3 = {
138+
} : {}
139+
containers_priority_s3 = local.enabled ? {
138140
for name, settings in var.containers :
139141
name => merge(settings, local.container_chamber[name], lookup(local.container_s3, name, {}))
140142
if local.enabled
141-
}
143+
} : {}
142144
}
143145

144146
data "aws_ssm_parameters_by_path" "default" {
@@ -302,27 +304,39 @@ module "ecs_alb_service_task" {
302304

303305
container_definition_json = jsonencode(local.container_definition)
304306

307+
# # When following latest, point service at the latest ACTIVE task definition ARN
308+
# task_definition = local.follow_latest_task_definition ? compact([local.latest_task_definition]) : []
305309
# This is set to true to allow ingress from the ALB sg
306310
use_alb_security_group = local.use_alb_security_group
307311
container_port = local.container_port
308312
alb_security_group = local.lb_sg_id
309-
security_group_ids = compact(concat([local.vpc_sg_id, local.rds_sg_id], local.external_security_group))
313+
security_group_ids = compact(concat([local.vpc_sg_id, local.rds_sg_id], local.external_security_group, var.additional_security_groups))
310314
enable_all_egress_rule = var.enable_all_egress_rule
311315

312316
nlb_cidr_blocks = local.is_nlb ? [module.vpc.outputs.vpc_cidr] : []
313317
nlb_container_port = local.is_nlb ? local.container_port : 80
314318
use_nlb_cidr_blocks = local.is_nlb
315319

316320
# See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#load_balancer
317-
ecs_load_balancers = local.use_lb ? [
318-
{
319-
container_name = try(local.service_container["name"], null),
320-
container_port = local.container_port,
321-
target_group_arn = local.is_alb ? module.alb_ingress[0].target_group_arn : local.nlb_compat.default_target_group_arn
322-
# not required since elb is unused but must be set to null
323-
elb_name = null
324-
},
325-
] : []
321+
ecs_load_balancers = local.use_lb ? concat(
322+
[
323+
{
324+
container_name = try(local.service_container["name"], null),
325+
container_port = local.container_port,
326+
target_group_arn = local.is_alb ? module.alb_ingress[0].target_group_arn : local.nlb_compat.default_target_group_arn
327+
# not required since elb is unused but must be set to null
328+
elb_name = null
329+
},
330+
],
331+
[
332+
for lb_config in var.additional_lb_target_groups : {
333+
container_name = lb_config.container_name
334+
container_port = lb_config.container_port
335+
target_group_arn = lb_config.target_group_arn
336+
elb_name = null
337+
}
338+
]
339+
) : []
326340

327341
assign_public_ip = local.assign_public_ip
328342
ignore_changes_task_definition = try(local.task["ignore_changes_task_definition"], false)
@@ -380,7 +394,7 @@ resource "aws_security_group_rule" "custom_sg_rules" {
380394
cidr_blocks = try(each.value.cidr_blocks, null)
381395
source_security_group_id = try(each.value.source_security_group_id, null)
382396
prefix_list_ids = try(each.value.prefix_list_ids, null)
383-
security_group_id = one(module.ecs_alb_service_task[*].service_security_group_id)
397+
security_group_id = try(each.value.security_group_id, null) != null ? each.value.security_group_id : one(module.ecs_alb_service_task[*].service_security_group_id)
384398
}
385399

386400
module "alb_ingress" {
@@ -618,8 +632,24 @@ data "aws_ecs_task_definition" "created_task" {
618632

619633
locals {
620634
created_task_definition = local.s3_mirroring_enabled ? data.aws_ecs_task_definition.created_task[0] : null
635+
636+
# Remove the 'image' field only from the service container to prevent drift when CI/CD updates images
637+
# CI/CD will provide the image for the service container in the complete task definition
638+
# Sidecar containers (datadog, fluent-bit, etc.) retain their images
639+
service_container_name = try(local.service_container["name"], null)
640+
641+
# Process each container: strip image from service container, keep it for sidecars
642+
container_definition_without_image = [
643+
for container in local.container_definition : merge(
644+
container,
645+
container.name == local.service_container_name ? { image = "" } : {}
646+
)
647+
]
648+
649+
650+
# Build the task template from the Terraform-created task definition
621651
task_template = local.s3_mirroring_enabled ? {
622-
containerDefinitions = local.container_definition
652+
containerDefinitions = local.container_definition_without_image
623653
family = lookup(local.created_task_definition, "family", null),
624654
taskRoleArn = lookup(local.created_task_definition, "task_role_arn", null),
625655
executionRoleArn = lookup(local.created_task_definition, "execution_role_arn", null),
@@ -628,11 +658,11 @@ locals {
628658
requiresCompatibilities = [lookup(local.task, "launch_type", "FARGATE")]
629659
cpu = tostring(lookup(local.task, "task_cpu", null))
630660
memory = tostring(lookup(local.task, "task_memory", null))
631-
632661
} : null
662+
633663
}
634664

635-
resource "aws_s3_bucket_object" "task_definition_template" {
665+
resource "aws_s3_object" "task_definition_template" {
636666
count = local.s3_mirroring_enabled ? 1 : 0
637667
bucket = lookup(module.s3[0].outputs, "bucket_id", null)
638668
key = format("%s/%s/task-template.json", module.ecs_cluster.outputs.cluster_name, module.this.id)

src/systems-manager.tf

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ variable "ssm_key_prefix" {
2626
locals {
2727
ssm_enabled = module.this.enabled && var.ssm_enabled
2828

29-
url_params = { for i, url in local.full_urls : format(var.ssm_key_format, var.ssm_key_prefix, var.name, "url/${i}") => {
29+
url_params = { for i, url in local.full_urls : format(var.ssm_key_format, var.ssm_key_prefix, join("-", concat([var.name], var.attributes)), "url/${i}") => {
3030
description = "ECS Service URL for ${var.name}"
3131
type = "String",
3232
value = url
@@ -56,6 +56,11 @@ resource "aws_ssm_parameter" "full_urls" {
5656
overwrite = true
5757

5858
tags = module.this.tags
59+
60+
# ignore changes to key_id, currently a workaround for terraform pinned version that allows permadrift.
61+
lifecycle {
62+
ignore_changes = [key_id]
63+
}
5964
}
6065

6166

src/variables.tf

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ variable "task" {
135135
circuit_breaker_deployment_enabled = optional(bool, true)
136136
circuit_breaker_rollback_enabled = optional(bool, true)
137137

138+
ignore_changes_task_definition = optional(bool, true)
139+
ignore_changes_desired_count = optional(bool, false)
140+
138141
ecs_service_enabled = optional(bool, true)
139142
bind_mount_volumes = optional(list(object({
140143
name = string
@@ -718,6 +721,7 @@ variable "custom_security_group_rules" {
718721
description = optional(string)
719722
source_security_group_id = optional(string)
720723
prefix_list_ids = optional(list(string))
724+
security_group_id = optional(string)
721725
}))
722726
description = "The list of custom security group rules to add to the service security group"
723727
default = []
@@ -756,3 +760,34 @@ variable "vpc_component_name" {
756760
description = "The name of a VPC component"
757761
default = "vpc"
758762
}
763+
764+
variable "additional_security_groups" {
765+
type = list(string)
766+
description = "A list of additional security group IDs to add to the service"
767+
default = []
768+
}
769+
770+
variable "additional_lb_target_groups" {
771+
type = list(object({
772+
container_name = string
773+
container_port = number
774+
target_group_arn = string
775+
}))
776+
description = <<-EOT
777+
Additional load balancer target group configurations for registering multiple container ports.
778+
This allows you to register sidecar containers to separate target groups.
779+
Each entry requires:
780+
- container_name: Name of the container to register
781+
- container_port: Port on the container to register
782+
- target_group_arn: ARN of the target group. Each additional port must specify a unique target group ARN
783+
EOT
784+
default = []
785+
786+
validation {
787+
condition = alltrue([
788+
for config in var.additional_lb_target_groups :
789+
can(regex("^arn:aws[a-z-]*:elasticloadbalancing:[a-z0-9-]+:[0-9]{12}:targetgroup/.+$", config.target_group_arn))
790+
])
791+
error_message = "Each additional_lb_target_groups entry must specify a valid target_group_arn in the format: arn:aws:elasticloadbalancing:region:account-id:targetgroup/name/id"
792+
}
793+
}

0 commit comments

Comments
 (0)