From 3dbfc6bde00d263894fa6c2d48cf24b10b40f08d Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Thu, 23 Oct 2025 11:36:16 -0500 Subject: [PATCH 01/10] feat: Add support for `managed_instances_provider` argument --- README.md | 3 +- main.tf | 1 + modules/cluster/README.md | 3 +- modules/cluster/main.tf | 204 ++++++++++++++++++++++++++++++++--- modules/cluster/variables.tf | 91 +++++++++++++++- variables.tf | 91 +++++++++++++++- wrappers/cluster/main.tf | 1 + wrappers/main.tf | 1 + 8 files changed, 376 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 48487cf6..ff317113 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,8 @@ No resources. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [autoscaling\_capacity\_providers](#input\_autoscaling\_capacity\_providers) | Map of autoscaling capacity provider definitions to create for the cluster |
map(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | +| [autoscaling\_capacity\_providers](#input\_autoscaling\_capacity\_providers) | [DEPRECATED - use `capacity_providers` instead] Map of autoscaling capacity provider definitions to create for the cluster |
map(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | +| [capacity\_providers](#input\_capacity\_providers) | Map of capacity provider definitions to create for the cluster |
map(object({
autoscaling_group_provider = optional(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
}))
managed_instances_provider = optional(object({
infrastructure_role_arn = optional(string)
instance_launch_template = object({
ec2_instance_profile_arn = optional(string)
instance_requirements = optional(object({
accelerator_count = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_manufacturers = optional(list(string))
accelerator_names = optional(list(string))
accelerator_total_memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_types = optional(list(string))
allowed_instance_types = optional(list(string))
bare_metal = optional(string)
baseline_ebs_bandwidth_mbps = optional(object({
max = optional(number)
min = optional(number)
}))
burstable_performance = optional(string)
cpu_manufacturers = optional(list(string))
excluded_instance_types = optional(list(string))
instance_generations = optional(list(string))
local_storage = optional(string)
local_storage_types = optional(list(string))
max_spot_price_as_percentage_of_optimal_on_demand_price = optional(number)
memory_gib_per_vcpu = optional(object({
max = optional(number)
min = optional(number)
}))
memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
network_bandwidth_gbps = optional(object({
max = optional(number)
min = optional(number)
}))
network_interface_count = optional(object({
max = optional(number)
min = optional(number)
}))
on_demand_max_price_percentage_over_lowest_price = optional(number)
require_hibernate_support = optional(bool)
spot_max_price_percentage_over_lowest_price = optional(number)
total_local_storage_gb = optional(object({
max = optional(number)
min = optional(number)
}))
vcpu_count = optional(object({
max = optional(number)
min = optional(number)
}))
}))
monitoring = optional(string)
network_configuration = optional(object({
security_groups = optional(list(string))
subnets = list(string)
}))
storage_configuration = optional(object({
storage_size_gib = number
}))
})
propagate_tags = optional(string)
}))
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | | [cloudwatch\_log\_group\_class](#input\_cloudwatch\_log\_group\_class) | Specified the log class of the log group. Possible values are: `STANDARD` or `INFREQUENT_ACCESS` | `string` | `null` | no | | [cloudwatch\_log\_group\_kms\_key\_id](#input\_cloudwatch\_log\_group\_kms\_key\_id) | If a KMS Key ARN is set, this key will be used to encrypt the corresponding log group. Please be sure that the KMS Key has an appropriate key policy (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html) | `string` | `null` | no | | [cloudwatch\_log\_group\_name](#input\_cloudwatch\_log\_group\_name) | Custom name of CloudWatch Log Group for ECS cluster | `string` | `null` | no | diff --git a/main.tf b/main.tf index 8833edff..fe3bf240 100644 --- a/main.tf +++ b/main.tf @@ -24,6 +24,7 @@ module "cluster" { # Cluster capacity providers autoscaling_capacity_providers = var.autoscaling_capacity_providers + capacity_providers = var.capacity_providers default_capacity_provider_strategy = var.default_capacity_provider_strategy # Task execution IAM role diff --git a/modules/cluster/README.md b/modules/cluster/README.md index 793a042d..897723c5 100644 --- a/modules/cluster/README.md +++ b/modules/cluster/README.md @@ -166,7 +166,8 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [autoscaling\_capacity\_providers](#input\_autoscaling\_capacity\_providers) | Map of autoscaling capacity provider definitions to create for the cluster |
map(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | +| [autoscaling\_capacity\_providers](#input\_autoscaling\_capacity\_providers) | [DEPRECATED - use `capacity_providers` instead] Map of autoscaling capacity provider definitions to create for the cluster |
map(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | +| [capacity\_providers](#input\_capacity\_providers) | Map of capacity provider definitions to create for the cluster |
map(object({
autoscaling_group_provider = optional(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
}))
managed_instances_provider = optional(object({
infrastructure_role_arn = optional(string)
instance_launch_template = object({
ec2_instance_profile_arn = optional(string)
instance_requirements = optional(object({
accelerator_count = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_manufacturers = optional(list(string))
accelerator_names = optional(list(string))
accelerator_total_memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_types = optional(list(string))
allowed_instance_types = optional(list(string))
bare_metal = optional(string)
baseline_ebs_bandwidth_mbps = optional(object({
max = optional(number)
min = optional(number)
}))
burstable_performance = optional(string)
cpu_manufacturers = optional(list(string))
excluded_instance_types = optional(list(string))
instance_generations = optional(list(string))
local_storage = optional(string)
local_storage_types = optional(list(string))
max_spot_price_as_percentage_of_optimal_on_demand_price = optional(number)
memory_gib_per_vcpu = optional(object({
max = optional(number)
min = optional(number)
}))
memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
network_bandwidth_gbps = optional(object({
max = optional(number)
min = optional(number)
}))
network_interface_count = optional(object({
max = optional(number)
min = optional(number)
}))
on_demand_max_price_percentage_over_lowest_price = optional(number)
require_hibernate_support = optional(bool)
spot_max_price_percentage_over_lowest_price = optional(number)
total_local_storage_gb = optional(object({
max = optional(number)
min = optional(number)
}))
vcpu_count = optional(object({
max = optional(number)
min = optional(number)
}))
}))
monitoring = optional(string)
network_configuration = optional(object({
security_groups = optional(list(string))
subnets = list(string)
}))
storage_configuration = optional(object({
storage_size_gib = number
}))
})
propagate_tags = optional(string)
}))
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | | [cloudwatch\_log\_group\_class](#input\_cloudwatch\_log\_group\_class) | Specified the log class of the log group. Possible values are: `STANDARD` or `INFREQUENT_ACCESS` | `string` | `null` | no | | [cloudwatch\_log\_group\_kms\_key\_id](#input\_cloudwatch\_log\_group\_kms\_key\_id) | If a KMS Key ARN is set, this key will be used to encrypt the corresponding log group. Please be sure that the KMS Key has an appropriate key policy (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html) | `string` | `null` | no | | [cloudwatch\_log\_group\_name](#input\_cloudwatch\_log\_group\_name) | Custom name of CloudWatch Log Group for ECS cluster | `string` | `null` | no | diff --git a/modules/cluster/main.tf b/modules/cluster/main.tf index 6eabcc3d..4ef0901b 100644 --- a/modules/cluster/main.tf +++ b/modules/cluster/main.tf @@ -123,34 +123,208 @@ resource "aws_ecs_cluster_capacity_providers" "this" { } ################################################################################ -# Capacity Provider - Autoscaling Group(s) +# Capacity Provider ################################################################################ +locals { + # TODO - embed the `autoscaling_capacity_providers` into a shape acceptable for + # `var.capacity_providers` so that it can be merged with the new `capacity_providers` + # for backward compatibility. Remove `autoscaling_capacity_providers` in the next major version. + capacity_providers = var.autoscaling_capacity_providers != null ? merge({ + for k, v in var.autoscaling_capacity_providers : k => { + autoscaling_group_provider = { + autoscaling_group_arn = v.autoscaling_group_arn + managed_draining = try(v.managed_draining, null) + managed_scaling = try(v.managed_draining, null) != null ? { + instance_warmup_period = try(v.managed_scaling.instance_warmup_period, null) + maximum_scaling_step_size = try(v.managed_scaling.maximum_scaling_step_size, null) + minimum_scaling_step_size = try(v.managed_scaling.minimum_scaling_step_size, null) + status = try(v.managed_scaling.status, null) + target_capacity = try(v.managed_scaling.target_capacity, null) + } : null + managed_termination_protection = try(v.managed_termination_protection, null) + } + managed_instances_provider = null + name = try(v.name, k) + tags = try(v.tags, {}) + } + }, var.capacity_providers != null ? var.capacity_providers : {}) : var.capacity_providers +} + resource "aws_ecs_capacity_provider" "this" { - for_each = var.create && var.autoscaling_capacity_providers != null ? var.autoscaling_capacity_providers : {} + for_each = var.create && local.capacity_providers != null ? local.capacity_providers : {} region = var.region - auto_scaling_group_provider { - auto_scaling_group_arn = each.value.auto_scaling_group_arn - managed_draining = each.value.managed_draining + dynamic "auto_scaling_group_provider" { + for_each = each.value.auto_scaling_group_provider != null ? [each.value.auto_scaling_group_provider] : [] - dynamic "managed_scaling" { - for_each = each.value.managed_scaling != null ? [each.value.managed_scaling] : [] + content { + auto_scaling_group_arn = each.value.auto_scaling_group_arn + managed_draining = each.value.managed_draining + + dynamic "managed_scaling" { + for_each = each.value.managed_scaling != null ? [each.value.managed_scaling] : [] - content { - instance_warmup_period = managed_scaling.value.instance_warmup_period - maximum_scaling_step_size = managed_scaling.value.maximum_scaling_step_size - minimum_scaling_step_size = managed_scaling.value.minimum_scaling_step_size - status = managed_scaling.value.status - target_capacity = managed_scaling.value.target_capacity + content { + instance_warmup_period = managed_scaling.value.instance_warmup_period + maximum_scaling_step_size = managed_scaling.value.maximum_scaling_step_size + minimum_scaling_step_size = managed_scaling.value.minimum_scaling_step_size + status = managed_scaling.value.status + target_capacity = managed_scaling.value.target_capacity + } } + + # When you use managed termination protection, you must also use managed scaling otherwise managed termination protection won't work + managed_termination_protection = each.value.managed_scaling != null ? each.value.managed_termination_protection : "DISABLED" } + } + + dynamic "managed_instances_provider" { + for_each = each.value.managed_instances_provider != null ? [each.value.managed_instances_provider] : [] + + content { + infrastructure_role_arn = managed_instances_provider.value.infrastructure_role_arn + + dynamic "instance_launch_template" { + for_each = managed_instances_provider.value.instance_launch_template != null ? [managed_instances_provider.value.instance_launch_template] : [] - # When you use managed termination protection, you must also use managed scaling otherwise managed termination protection won't work - managed_termination_protection = each.value.managed_scaling != null ? each.value.managed_termination_protection : "DISABLED" + content { + ec2_instance_profile_arn = instance_launch_template.value.ec2_instance_profile_arn + + dynamic "instance_requirements" { + for_each = instance_launch_template.value.instance_requirements != null ? [instance_launch_template.value.instance_requirements] : [] + + content { + dynamic "accelerator_count" { + for_each = instance_requirements.value.accelerator_count != null ? [instance_requirements.value.accelerator_count] : [] + + content { + max = accelerator_count.value.max + min = accelerator_count.value.min + } + } + + accelerator_manufacturers = instance_requirements.value.accelerator_manufacturers + accelerator_names = instance_requirements.value.accelerator_names + + dynamic "accelerator_total_memory_mib" { + for_each = instance_requirements.value.accelerator_total_memory_mib != null ? [instance_requirements.value.accelerator_total_memory_mib] : [] + + content { + max = accelerator_total_memory_mib.value.max + min = accelerator_total_memory_mib.value.min + } + } + + accelerator_types = instance_requirements.value.accelerator_types + allowed_instance_types = instance_requirements.value.allowed_instance_types + bare_metal = instance_requirements.value.bare_metal + + dynamic "baseline_ebs_bandwidth_mbps" { + for_each = instance_requirements.value.baseline_ebs_bandwidth_mbps != null ? [instance_requirements.value.baseline_ebs_bandwidth_mbps] : [] + + content { + max = baseline_ebs_bandwidth_mbps.value.max + min = baseline_ebs_bandwidth_mbps.value.min + } + } + + burstable_performance = instance_requirements.value.burstable_performance + cpu_manufacturers = instance_requirements.value.cpu_manufacturers + excluded_instance_types = instance_requirements.value.excluded_instance_types + instance_generations = instance_requirements.value.instance_generations + local_storage = instance_requirements.value.local_storage + local_storage_types = instance_requirements.value.local_storage_types + max_spot_price_as_percentage_of_optimal_on_demand_price = instance_requirements.value.max_spot_price_as_percentage_of_optimal_on_demand_price + + dynamic "memory_gib_per_vcpu" { + for_each = instance_requirements.value.memory_gib_per_vcpu != null ? [instance_requirements.value.memory_gib_per_vcpu] : [] + + content { + max = memory_gib_per_vcpu.value.max + min = memory_gib_per_vcpu.value.min + } + } + + dynamic "memory_mib" { + for_each = instance_requirements.value.memory_mib != null ? [instance_requirements.value.memory_mib] : [] + + content { + max = memory_mib.value.max + min = memory_mib.value.min + } + } + + dynamic "network_bandwidth_gbps" { + for_each = instance_requirements.value.network_bandwidth_gbps != null ? [instance_requirements.value.network_bandwidth_gbps] : [] + + content { + max = network_bandwidth_gbps.value.max + min = network_bandwidth_gbps.value.min + } + } + + dynamic "network_interface_count" { + for_each = instance_requirements.value.network_interface_count != null ? [instance_requirements.value.network_interface_count] : [] + + content { + max = network_interface_count.value.max + min = network_interface_count.value.min + } + } + + on_demand_max_price_percentage_over_lowest_price = instance_requirements.value.on_demand_max_price_percentage_over_lowest_price + require_hibernate_support = instance_requirements.value.require_hibernate_support + spot_max_price_percentage_over_lowest_price = instance_requirements.value.spot_max_price_percentage_over_lowest_price + + dynamic "total_local_storage_gb" { + for_each = instance_requirements.value.total_local_storage_gb != null ? [instance_requirements.value.total_local_storage_gb] : [] + + content { + max = total_local_storage_gb.value.max + min = total_local_storage_gb.value.min + } + } + + dynamic "vcpu_count" { + for_each = instance_requirements.value.vcpu_count != null ? [instance_requirements.value.vcpu_count] : [] + + content { + max = vcpu_count.value.max + min = vcpu_count.value.min + } + } + } + } + + monitoring = instance_launch_template.value.monitoring + + dynamic "network_configuration" { + for_each = instance_launch_template.value.network_configuration != null ? [instance_launch_template.value.network_configuration] : [] + + content { + security_groups = network_configuration.value.security_groups + subnets = network_configuration.value.subnets + } + } + + dynamic "storage_configuration" { + for_each = instance_launch_template.value.storage_configuration != null ? [instance_launch_template.value.storage_configuration] : [] + + content { + storage_size_gib = storage_configuration.value.storage_size_gib + } + } + } + } + + propagate_tags = managed_instances_provider.value.propagate_tags + } } + cluster = each.value.auto_scaling_group_provider == null ? aws_ecs_cluster.this[0].id : null + name = try(coalesce(each.value.name, each.key), "") tags = merge( diff --git a/modules/cluster/variables.tf b/modules/cluster/variables.tf index 495e5dda..4dac0c4b 100644 --- a/modules/cluster/variables.tf +++ b/modules/cluster/variables.tf @@ -122,7 +122,7 @@ variable "cloudwatch_log_group_tags" { ################################################################################ variable "autoscaling_capacity_providers" { - description = "Map of autoscaling capacity provider definitions to create for the cluster" + description = "[DEPRECATED - use `capacity_providers` instead] Map of autoscaling capacity provider definitions to create for the cluster" type = map(object({ auto_scaling_group_arn = string managed_draining = optional(string, "ENABLED") @@ -140,6 +140,95 @@ variable "autoscaling_capacity_providers" { default = null } +variable "capacity_providers" { + description = "Map of capacity provider definitions to create for the cluster" + type = map(object({ + autoscaling_group_provider = optional(object({ + auto_scaling_group_arn = string + managed_draining = optional(string, "ENABLED") + managed_scaling = optional(object({ + instance_warmup_period = optional(number) + maximum_scaling_step_size = optional(number) + minimum_scaling_step_size = optional(number) + status = optional(string) + target_capacity = optional(number) + })) + managed_termination_protection = optional(string) + })) + managed_instances_provider = optional(object({ + infrastructure_role_arn = optional(string) + instance_launch_template = object({ + ec2_instance_profile_arn = optional(string) + instance_requirements = optional(object({ + accelerator_count = optional(object({ + max = optional(number) + min = optional(number) + })) + accelerator_manufacturers = optional(list(string)) + accelerator_names = optional(list(string)) + accelerator_total_memory_mib = optional(object({ + max = optional(number) + min = optional(number) + })) + accelerator_types = optional(list(string)) + allowed_instance_types = optional(list(string)) + bare_metal = optional(string) + baseline_ebs_bandwidth_mbps = optional(object({ + max = optional(number) + min = optional(number) + })) + burstable_performance = optional(string) + cpu_manufacturers = optional(list(string)) + excluded_instance_types = optional(list(string)) + instance_generations = optional(list(string)) + local_storage = optional(string) + local_storage_types = optional(list(string)) + max_spot_price_as_percentage_of_optimal_on_demand_price = optional(number) + memory_gib_per_vcpu = optional(object({ + max = optional(number) + min = optional(number) + })) + memory_mib = optional(object({ + max = optional(number) + min = optional(number) + })) + network_bandwidth_gbps = optional(object({ + max = optional(number) + min = optional(number) + })) + network_interface_count = optional(object({ + max = optional(number) + min = optional(number) + })) + on_demand_max_price_percentage_over_lowest_price = optional(number) + require_hibernate_support = optional(bool) + spot_max_price_percentage_over_lowest_price = optional(number) + total_local_storage_gb = optional(object({ + max = optional(number) + min = optional(number) + })) + vcpu_count = optional(object({ + max = optional(number) + min = optional(number) + })) + })) + monitoring = optional(string) + network_configuration = optional(object({ + security_groups = optional(list(string)) + subnets = list(string) + })) + storage_configuration = optional(object({ + storage_size_gib = number + })) + }) + propagate_tags = optional(string) + })) + name = optional(string) # Will fall back to use map key if not set + tags = optional(map(string), {}) + })) + default = null +} + variable "default_capacity_provider_strategy" { description = "Map of default capacity provider strategy definitions to use for the cluster" type = map(object({ diff --git a/variables.tf b/variables.tf index 5c354d6f..26bf1671 100644 --- a/variables.tf +++ b/variables.tf @@ -127,7 +127,7 @@ variable "cloudwatch_log_group_tags" { ################################################################################ variable "autoscaling_capacity_providers" { - description = "Map of autoscaling capacity provider definitions to create for the cluster" + description = "[DEPRECATED - use `capacity_providers` instead] Map of autoscaling capacity provider definitions to create for the cluster" type = map(object({ auto_scaling_group_arn = string managed_draining = optional(string, "ENABLED") @@ -145,6 +145,95 @@ variable "autoscaling_capacity_providers" { default = null } +variable "capacity_providers" { + description = "Map of capacity provider definitions to create for the cluster" + type = map(object({ + autoscaling_group_provider = optional(object({ + auto_scaling_group_arn = string + managed_draining = optional(string, "ENABLED") + managed_scaling = optional(object({ + instance_warmup_period = optional(number) + maximum_scaling_step_size = optional(number) + minimum_scaling_step_size = optional(number) + status = optional(string) + target_capacity = optional(number) + })) + managed_termination_protection = optional(string) + })) + managed_instances_provider = optional(object({ + infrastructure_role_arn = optional(string) + instance_launch_template = object({ + ec2_instance_profile_arn = optional(string) + instance_requirements = optional(object({ + accelerator_count = optional(object({ + max = optional(number) + min = optional(number) + })) + accelerator_manufacturers = optional(list(string)) + accelerator_names = optional(list(string)) + accelerator_total_memory_mib = optional(object({ + max = optional(number) + min = optional(number) + })) + accelerator_types = optional(list(string)) + allowed_instance_types = optional(list(string)) + bare_metal = optional(string) + baseline_ebs_bandwidth_mbps = optional(object({ + max = optional(number) + min = optional(number) + })) + burstable_performance = optional(string) + cpu_manufacturers = optional(list(string)) + excluded_instance_types = optional(list(string)) + instance_generations = optional(list(string)) + local_storage = optional(string) + local_storage_types = optional(list(string)) + max_spot_price_as_percentage_of_optimal_on_demand_price = optional(number) + memory_gib_per_vcpu = optional(object({ + max = optional(number) + min = optional(number) + })) + memory_mib = optional(object({ + max = optional(number) + min = optional(number) + })) + network_bandwidth_gbps = optional(object({ + max = optional(number) + min = optional(number) + })) + network_interface_count = optional(object({ + max = optional(number) + min = optional(number) + })) + on_demand_max_price_percentage_over_lowest_price = optional(number) + require_hibernate_support = optional(bool) + spot_max_price_percentage_over_lowest_price = optional(number) + total_local_storage_gb = optional(object({ + max = optional(number) + min = optional(number) + })) + vcpu_count = optional(object({ + max = optional(number) + min = optional(number) + })) + })) + monitoring = optional(string) + network_configuration = optional(object({ + security_groups = optional(list(string)) + subnets = list(string) + })) + storage_configuration = optional(object({ + storage_size_gib = number + })) + }) + propagate_tags = optional(string) + })) + name = optional(string) # Will fall back to use map key if not set + tags = optional(map(string), {}) + })) + default = null +} + variable "default_capacity_provider_strategy" { description = "Map of default capacity provider strategy definitions to use for the cluster" type = map(object({ diff --git a/wrappers/cluster/main.tf b/wrappers/cluster/main.tf index bbd27c5d..70590835 100644 --- a/wrappers/cluster/main.tf +++ b/wrappers/cluster/main.tf @@ -4,6 +4,7 @@ module "wrapper" { for_each = var.items autoscaling_capacity_providers = try(each.value.autoscaling_capacity_providers, var.defaults.autoscaling_capacity_providers, null) + capacity_providers = try(each.value.capacity_providers, var.defaults.capacity_providers, null) cloudwatch_log_group_class = try(each.value.cloudwatch_log_group_class, var.defaults.cloudwatch_log_group_class, null) cloudwatch_log_group_kms_key_id = try(each.value.cloudwatch_log_group_kms_key_id, var.defaults.cloudwatch_log_group_kms_key_id, null) cloudwatch_log_group_name = try(each.value.cloudwatch_log_group_name, var.defaults.cloudwatch_log_group_name, null) diff --git a/wrappers/main.tf b/wrappers/main.tf index 13659ca0..0c27c2a8 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -4,6 +4,7 @@ module "wrapper" { for_each = var.items autoscaling_capacity_providers = try(each.value.autoscaling_capacity_providers, var.defaults.autoscaling_capacity_providers, null) + capacity_providers = try(each.value.capacity_providers, var.defaults.capacity_providers, null) cloudwatch_log_group_class = try(each.value.cloudwatch_log_group_class, var.defaults.cloudwatch_log_group_class, null) cloudwatch_log_group_kms_key_id = try(each.value.cloudwatch_log_group_kms_key_id, var.defaults.cloudwatch_log_group_kms_key_id, null) cloudwatch_log_group_name = try(each.value.cloudwatch_log_group_name, var.defaults.cloudwatch_log_group_name, null) From f3ae9c73db04b167af797b5ee7aa9711981d0377 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Thu, 23 Oct 2025 14:18:06 -0500 Subject: [PATCH 02/10] feat: Add support for the ECS Managed Instances IAM role, instance profile, and policy --- README.md | 27 ++++ main.tf | 26 ++++ modules/cluster/README.md | 46 +++++- modules/cluster/main.tf | 262 ++++++++++++++++++++++++++++++++++- modules/cluster/outputs.tf | 71 +++++++++- modules/cluster/variables.tf | 148 ++++++++++++++++++++ modules/service/README.md | 2 +- modules/service/variables.tf | 4 +- outputs.tf | 45 ++++++ variables.tf | 148 ++++++++++++++++++++ wrappers/cluster/main.tf | 34 +++-- wrappers/main.tf | 56 +++++--- 12 files changed, 825 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index ff317113..29b7edfa 100644 --- a/README.md +++ b/README.md @@ -191,9 +191,27 @@ No resources. | [cluster\_tags](#input\_cluster\_tags) | A map of additional tags to add to the cluster | `map(string)` | `{}` | no | | [create](#input\_create) | Determines whether resources will be created (affects all resources) | `bool` | `true` | no | | [create\_cloudwatch\_log\_group](#input\_create\_cloudwatch\_log\_group) | Determines whether a log group is created by this module for the cluster logs. If not, AWS will automatically create one if logging is enabled | `bool` | `true` | no | +| [create\_infrastructure\_iam\_role](#input\_create\_infrastructure\_iam\_role) | Determines whether the ECS infrastructure IAM role should be created | `bool` | `true` | no | +| [create\_node\_iam\_instance\_profile](#input\_create\_node\_iam\_instance\_profile) | Determines whether an IAM instance profile is created or to use an existing IAM instance profile | `bool` | `true` | no | | [create\_task\_exec\_iam\_role](#input\_create\_task\_exec\_iam\_role) | Determines whether the ECS task definition IAM role should be created | `bool` | `false` | no | | [create\_task\_exec\_policy](#input\_create\_task\_exec\_policy) | Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters | `bool` | `true` | no | | [default\_capacity\_provider\_strategy](#input\_default\_capacity\_provider\_strategy) | Map of default capacity provider strategy definitions to use for the cluster |
map(object({
base = optional(number)
name = optional(string) # Will fall back to use map key if not set
weight = optional(number)
}))
| `null` | no | +| [infrastructure\_iam\_role\_description](#input\_infrastructure\_iam\_role\_description) | Description of the role | `string` | `null` | no | +| [infrastructure\_iam\_role\_name](#input\_infrastructure\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | +| [infrastructure\_iam\_role\_path](#input\_infrastructure\_iam\_role\_path) | IAM role path | `string` | `null` | no | +| [infrastructure\_iam\_role\_permissions\_boundary](#input\_infrastructure\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [infrastructure\_iam\_role\_tags](#input\_infrastructure\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | +| [infrastructure\_iam\_role\_use\_name\_prefix](#input\_infrastructure\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`iam_role_name`) is used as a prefix | `bool` | `true` | no | +| [node\_iam\_role\_additional\_policies](#input\_node\_iam\_role\_additional\_policies) | Additional policies to be added to the IAM role | `map(string)` | `{}` | no | +| [node\_iam\_role\_description](#input\_node\_iam\_role\_description) | Description of the role | `string` | `"ECS Managed Instances node IAM role"` | no | +| [node\_iam\_role\_name](#input\_node\_iam\_role\_name) | Name to use on IAM role/instance profile created | `string` | `null` | no | +| [node\_iam\_role\_override\_policy\_documents](#input\_node\_iam\_role\_override\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid` | `list(string)` | `[]` | no | +| [node\_iam\_role\_path](#input\_node\_iam\_role\_path) | IAM role/instance profile path | `string` | `null` | no | +| [node\_iam\_role\_permissions\_boundary](#input\_node\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [node\_iam\_role\_source\_policy\_documents](#input\_node\_iam\_role\_source\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. Statements must have unique `sid`s | `list(string)` | `[]` | no | +| [node\_iam\_role\_statements](#input\_node\_iam\_role\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage |
map(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string, "Allow")
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
variable = string
values = list(string)
})))
}))
| `null` | no | +| [node\_iam\_role\_tags](#input\_node\_iam\_role\_tags) | A map of additional tags to add to the IAM role/instance profile created | `map(string)` | `{}` | no | +| [node\_iam\_role\_use\_name\_prefix](#input\_node\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role/instance profile name (`node_iam_role_name`) is used as a prefix | `bool` | `true` | no | | [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | | [services](#input\_services) | Map of service definitions to create |
map(object({
create = optional(bool)
create_service = optional(bool)
tags = optional(map(string))

# Service
ignore_task_definition_changes = optional(bool)
alarms = optional(object({
alarm_names = list(string)
enable = optional(bool)
rollback = optional(bool)
}))
availability_zone_rebalancing = optional(string)
capacity_provider_strategy = optional(map(object({
base = optional(number)
capacity_provider = string
weight = optional(number)
})))
deployment_circuit_breaker = optional(object({
enable = bool
rollback = bool
}))
deployment_configuration = optional(object({
strategy = optional(string)
bake_time_in_minutes = optional(string)
canary_configuration = optional(object({
canary_bake_time_in_minutes = optional(string)
canary_percent = optional(string)
}))
linear_configuration = optional(object({
step_bake_time_in_minutes = optional(string)
step_percent = optional(string)
}))
lifecycle_hook = optional(map(object({
hook_target_arn = string
role_arn = string
lifecycle_stages = list(string)
hook_details = optional(string)
})))
}))
deployment_controller = optional(object({
type = optional(string)
}))
deployment_maximum_percent = optional(number, 200)
deployment_minimum_healthy_percent = optional(number, 66)
desired_count = optional(number, 1)
enable_ecs_managed_tags = optional(bool)
enable_execute_command = optional(bool)
force_delete = optional(bool)
force_new_deployment = optional(bool)
health_check_grace_period_seconds = optional(number)
launch_type = optional(string)
load_balancer = optional(map(object({
container_name = string
container_port = number
elb_name = optional(string)
target_group_arn = optional(string)
advanced_configuration = optional(object({
alternate_target_group_arn = string
production_listener_rule = string
role_arn = string
test_listener_rule = optional(string)
}))
})))
name = optional(string) # Will fall back to use map key if not set
assign_public_ip = optional(bool)
security_group_ids = optional(list(string))
subnet_ids = optional(list(string))
ordered_placement_strategy = optional(map(object({
field = optional(string)
type = string
})))
placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
platform_version = optional(string)
propagate_tags = optional(string)
scheduling_strategy = optional(string)
service_connect_configuration = optional(object({
enabled = optional(bool)
log_configuration = optional(object({
log_driver = string
options = optional(map(string))
secret_option = optional(list(object({
name = string
value_from = string
})))
}))
namespace = optional(string)
service = optional(list(object({
client_alias = optional(object({
dns_name = optional(string)
port = number
test_traffic_rules = optional(list(object({
header = optional(object({
name = string
value = object({
exact = string
})
}))
})))
}))
discovery_name = optional(string)
ingress_port_override = optional(number)
port_name = string
timeout = optional(object({
idle_timeout_seconds = optional(number)
per_request_timeout_seconds = optional(number)
}))
tls = optional(object({
issuer_cert_authority = object({
aws_pca_authority_arn = string
})
kms_key = optional(string)
role_arn = optional(string)
}))
})))
}))
service_registries = optional(object({
container_name = optional(string)
container_port = optional(number)
port = optional(number)
registry_arn = string
}))
sigint_rollback = optional(bool)
timeouts = optional(object({
create = optional(string)
update = optional(string)
delete = optional(string)
}))
triggers = optional(map(string))
volume_configuration = optional(object({
name = string
managed_ebs_volume = object({
encrypted = optional(bool)
file_system_type = optional(string)
iops = optional(number)
kms_key_id = optional(string)
size_in_gb = optional(number)
snapshot_id = optional(string)
tag_specifications = optional(list(object({
propagate_tags = optional(string)
resource_type = string
tags = optional(map(string))
})))
throughput = optional(number)
volume_type = optional(string)
})
}))
vpc_lattice_configurations = optional(object({
role_arn = string
target_group_arn = string
port_name = string
}))
wait_for_steady_state = optional(bool)
service_tags = optional(map(string))
# Service - IAM Role
create_iam_role = optional(bool)
iam_role_arn = optional(string)
iam_role_name = optional(string)
iam_role_use_name_prefix = optional(bool)
iam_role_path = optional(string)
iam_role_description = optional(string)
iam_role_permissions_boundary = optional(string)
iam_role_tags = optional(map(string))
iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
# Task Definition
create_task_definition = optional(bool)
task_definition_arn = optional(string)
container_definitions = optional(map(object({
operating_system_family = optional(string)
tags = optional(map(string))

# Container definition
command = optional(list(string))
cpu = optional(number)
credentialSpecs = optional(list(string))
dependsOn = optional(list(object({
condition = string
containerName = string
})))
disableNetworking = optional(bool)
dnsSearchDomains = optional(list(string))
dnsServers = optional(list(string))
dockerLabels = optional(map(string))
dockerSecurityOptions = optional(list(string))
enable_execute_command = optional(bool)
entrypoint = optional(list(string))
environment = optional(list(object({
name = string
value = string
})))
environmentFiles = optional(list(object({
type = string
value = string
})))
essential = optional(bool)
extraHosts = optional(list(object({
hostname = string
ipAddress = string
})))
firelensConfiguration = optional(object({
options = optional(map(string))
type = optional(string)
}))
healthCheck = optional(object({
command = optional(list(string))
interval = optional(number)
retries = optional(number)
startPeriod = optional(number)
timeout = optional(number)
}))
hostname = optional(string)
image = optional(string)
interactive = optional(bool)
links = optional(list(string))
linuxParameters = optional(object({
capabilities = optional(object({
add = optional(list(string))
drop = optional(list(string))
}))
devices = optional(list(object({
containerPath = optional(string)
hostPath = optional(string)
permissions = optional(list(string))
})))
initProcessEnabled = optional(bool)
maxSwap = optional(number)
sharedMemorySize = optional(number)
swappiness = optional(number)
tmpfs = optional(list(object({
containerPath = string
mountOptions = optional(list(string))
size = number
})))
}))
logConfiguration = optional(object({
logDriver = optional(string)
options = optional(map(string))
secretOptions = optional(list(object({
name = string
valueFrom = string
})))
}))
memory = optional(number)
memoryReservation = optional(number)
mountPoints = optional(list(object({
containerPath = optional(string)
readOnly = optional(bool)
sourceVolume = optional(string)
})), [])
name = optional(string)
portMappings = optional(list(object({
appProtocol = optional(string)
containerPort = optional(number)
containerPortRange = optional(string)
hostPort = optional(number)
name = optional(string)
protocol = optional(string)
})), [])
privileged = optional(bool)
pseudoTerminal = optional(bool)
readonlyRootFilesystem = optional(bool)
repositoryCredentials = optional(object({
credentialsParameter = optional(string)
}))
resourceRequirements = optional(list(object({
type = string
value = string
})))
restartPolicy = optional(object({
enabled = optional(bool)
ignoredExitCodes = optional(list(number))
restartAttemptPeriod = optional(number)
}))
secrets = optional(list(object({
name = string
valueFrom = string
})))
startTimeout = optional(number)
stopTimeout = optional(number)
systemControls = optional(list(object({
namespace = optional(string)
value = optional(string)
})))
ulimits = optional(list(object({
hardLimit = number
name = string
softLimit = number
})))
user = optional(string)
versionConsistency = optional(string)
volumesFrom = optional(list(object({
readOnly = optional(bool)
sourceContainer = optional(string)
})))
workingDirectory = optional(string)

# Cloudwatch Log Group
service = optional(string, "")
enable_cloudwatch_logging = optional(bool)
create_cloudwatch_log_group = optional(bool)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool)
cloudwatch_log_group_class = optional(string)
cloudwatch_log_group_retention_in_days = optional(number)
cloudwatch_log_group_kms_key_id = optional(string)
})))
cpu = optional(number, 1024)
enable_fault_injection = optional(bool)
ephemeral_storage = optional(object({
size_in_gib = number
}))
family = optional(string)
ipc_mode = optional(string)
memory = optional(number, 2048)
network_mode = optional(string)
pid_mode = optional(string)
proxy_configuration = optional(object({
container_name = string
properties = optional(map(string))
type = optional(string)
}))
requires_compatibilities = optional(list(string))
runtime_platform = optional(object({
cpu_architecture = optional(string)
operating_system_family = optional(string)
}))
skip_destroy = optional(bool)
task_definition_placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
track_latest = optional(bool)
volume = optional(map(object({
configure_at_launch = optional(bool)
docker_volume_configuration = optional(object({
autoprovision = optional(bool)
driver = optional(string)
driver_opts = optional(map(string))
labels = optional(map(string))
scope = optional(string)
}))
efs_volume_configuration = optional(object({
authorization_config = optional(object({
access_point_id = optional(string)
iam = optional(string)
}))
file_system_id = string
root_directory = optional(string)
transit_encryption = optional(string)
transit_encryption_port = optional(number)
}))
fsx_windows_file_server_volume_configuration = optional(object({
authorization_config = optional(object({
credentials_parameter = string
domain = string
}))
file_system_id = string
root_directory = string
}))
host_path = optional(string)
name = optional(string)
})))
task_tags = optional(map(string))
# Task Execution - IAM Role
create_task_exec_iam_role = optional(bool)
task_exec_iam_role_arn = optional(string)
task_exec_iam_role_name = optional(string)
task_exec_iam_role_use_name_prefix = optional(bool)
task_exec_iam_role_path = optional(string)
task_exec_iam_role_description = optional(string)
task_exec_iam_role_permissions_boundary = optional(string)
task_exec_iam_role_tags = optional(map(string))
task_exec_iam_role_policies = optional(map(string))
task_exec_iam_role_max_session_duration = optional(number)
create_task_exec_policy = optional(bool)
task_exec_ssm_param_arns = optional(list(string))
task_exec_secret_arns = optional(list(string))
task_exec_iam_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
task_exec_iam_policy_path = optional(string)
# Tasks - IAM Role
create_tasks_iam_role = optional(bool)
tasks_iam_role_arn = optional(string)
tasks_iam_role_name = optional(string)
tasks_iam_role_use_name_prefix = optional(bool)
tasks_iam_role_path = optional(string)
tasks_iam_role_description = optional(string)
tasks_iam_role_permissions_boundary = optional(string)
tasks_iam_role_tags = optional(map(string))
tasks_iam_role_policies = optional(map(string))
tasks_iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
# Task Set
external_id = optional(string)
scale = optional(object({
unit = optional(string)
value = optional(number)
}))
wait_until_stable = optional(bool)
wait_until_stable_timeout = optional(string)
# Autoscaling
enable_autoscaling = optional(bool)
autoscaling_min_capacity = optional(number)
autoscaling_max_capacity = optional(number)
autoscaling_policies = optional(map(object({
name = optional(string) # Will fall back to the key name if not provided
policy_type = optional(string)
predictive_scaling_policy_configuration = optional(object({
max_capacity_breach_behavior = optional(string)
max_capacity_buffer = optional(number)
metric_specification = list(object({
customized_capacity_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_load_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_scaling_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
predefined_load_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_metric_pair_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_scaling_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
target_value = number
}))
mode = optional(string)
scheduling_buffer_time = optional(number)
}))
step_scaling_policy_configuration = optional(object({
adjustment_type = optional(string)
cooldown = optional(number)
metric_aggregation_type = optional(string)
min_adjustment_magnitude = optional(number)
step_adjustment = optional(list(object({
metric_interval_lower_bound = optional(string)
metric_interval_upper_bound = optional(string)
scaling_adjustment = number
})))
}))
target_tracking_scaling_policy_configuration = optional(object({
customized_metric_specification = optional(object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
metrics = optional(list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = string
namespace = string
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
})))
namespace = optional(string)
statistic = optional(string)
unit = optional(string)
}))

disable_scale_in = optional(bool)
predefined_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
scale_in_cooldown = optional(number)
scale_out_cooldown = optional(number)
target_value = optional(number)
}))
})))
autoscaling_scheduled_actions = optional(map(object({
name = optional(string)
min_capacity = number
max_capacity = number
schedule = string
start_time = optional(string)
end_time = optional(string)
timezone = optional(string)
})))
autoscaling_suspended_state = optional(object({
dynamic_scaling_in_suspended = optional(bool)
dynamic_scaling_out_suspended = optional(bool)
scheduled_scaling_suspended = optional(bool)
}))
# Security Group
create_security_group = optional(bool)
vpc_id = optional(string)
security_group_name = optional(string)
security_group_use_name_prefix = optional(bool)
security_group_description = optional(string)
security_group_ingress_rules = optional(map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string))
to_port = optional(string)
})))
security_group_egress_rules = optional(map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string))
to_port = optional(string)
})))
security_group_tags = optional(map(string))
# ECS Infrastructure IAM Role
create_infrastructure_iam_role = optional(bool)
infrastructure_iam_role_arn = optional(string)
infrastructure_iam_role_name = optional(string)
infrastructure_iam_role_use_name_prefix = optional(bool)
infrastructure_iam_role_path = optional(string)
infrastructure_iam_role_description = optional(string)
infrastructure_iam_role_permissions_boundary = optional(string)
infrastructure_iam_role_tags = optional(map(string))
}))
| `null` | no | | [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | @@ -219,6 +237,15 @@ No resources. | [cluster\_capacity\_providers](#output\_cluster\_capacity\_providers) | Map of cluster capacity providers attributes | | [cluster\_id](#output\_cluster\_id) | ID that identifies the cluster | | [cluster\_name](#output\_cluster\_name) | Name that identifies the cluster | +| [infrastructure\_iam\_role\_arn](#output\_infrastructure\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [infrastructure\_iam\_role\_name](#output\_infrastructure\_iam\_role\_name) | IAM role name | +| [infrastructure\_iam\_role\_unique\_id](#output\_infrastructure\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | +| [node\_iam\_instance\_profile\_arn](#output\_node\_iam\_instance\_profile\_arn) | ARN assigned by AWS to the instance profile | +| [node\_iam\_instance\_profile\_id](#output\_node\_iam\_instance\_profile\_id) | Instance profile's ID | +| [node\_iam\_instance\_profile\_unique](#output\_node\_iam\_instance\_profile\_unique) | Stable and unique string identifying the IAM instance profile | +| [node\_iam\_role\_arn](#output\_node\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [node\_iam\_role\_name](#output\_node\_iam\_role\_name) | IAM role name | +| [node\_iam\_role\_unique\_id](#output\_node\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | | [services](#output\_services) | Map of services created and their attributes | | [task\_exec\_iam\_role\_arn](#output\_task\_exec\_iam\_role\_arn) | Task execution IAM role ARN | | [task\_exec\_iam\_role\_name](#output\_task\_exec\_iam\_role\_name) | Task execution IAM role name | diff --git a/main.tf b/main.tf index fe3bf240..8d605fdc 100644 --- a/main.tf +++ b/main.tf @@ -43,6 +43,32 @@ module "cluster" { task_exec_secret_arns = var.task_exec_secret_arns task_exec_iam_statements = var.task_exec_iam_statements + # -- ECS Managed Instances -- + + # Infrastructure IAM role + create_infrastructure_iam_role = var.create_infrastructure_iam_role + infrastructure_iam_role_name = var.infrastructure_iam_role_name + infrastructure_iam_role_use_name_prefix = var.infrastructure_iam_role_use_name_prefix + infrastructure_iam_role_path = var.infrastructure_iam_role_path + infrastructure_iam_role_description = var.infrastructure_iam_role_description + infrastructure_iam_role_permissions_boundary = var.infrastructure_iam_role_permissions_boundary + infrastructure_iam_role_tags = var.infrastructure_iam_role_tags + + # Node IAM role & instance profile + create_node_iam_instance_profile = var.create_node_iam_instance_profile + node_iam_role_name = var.node_iam_role_name + node_iam_role_use_name_prefix = var.node_iam_role_use_name_prefix + node_iam_role_path = var.node_iam_role_path + node_iam_role_description = var.node_iam_role_description + node_iam_role_permissions_boundary = var.node_iam_role_permissions_boundary + node_iam_role_additional_policies = var.node_iam_role_additional_policies + node_iam_role_tags = var.node_iam_role_tags + + # Node IAM role policy + node_iam_role_source_policy_documents = var.node_iam_role_source_policy_documents + node_iam_role_override_policy_documents = var.node_iam_role_override_policy_documents + node_iam_role_statements = var.node_iam_role_statements + tags = merge(var.tags, var.cluster_tags) } diff --git a/modules/cluster/README.md b/modules/cluster/README.md index 897723c5..e9070a3b 100644 --- a/modules/cluster/README.md +++ b/modules/cluster/README.md @@ -155,12 +155,25 @@ No modules. | [aws_ecs_capacity_provider.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_capacity_provider) | resource | | [aws_ecs_cluster.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster) | resource | | [aws_ecs_cluster_capacity_providers.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster_capacity_providers) | resource | +| [aws_iam_instance_profile.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | +| [aws_iam_policy.node](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_policy.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.node](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.infrastructure_managed_instances](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.node](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.node_additional](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.task_exec_additional](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.node](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.node_assume_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.task_exec_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | ## Inputs @@ -176,10 +189,28 @@ No modules. | [configuration](#input\_configuration) | The execute command configuration for the cluster |
object({
execute_command_configuration = optional(object({
kms_key_id = optional(string)
log_configuration = optional(object({
cloud_watch_encryption_enabled = optional(bool)
cloud_watch_log_group_name = optional(string)
s3_bucket_encryption_enabled = optional(bool)
s3_bucket_name = optional(string)
s3_kms_key_id = optional(string)
s3_key_prefix = optional(string)
}))
logging = optional(string, "OVERRIDE")
}))
managed_storage_configuration = optional(object({
fargate_ephemeral_storage_kms_key_id = optional(string)
kms_key_id = optional(string)
}))
})
|
{
"execute_command_configuration": {
"log_configuration": {
"cloud_watch_log_group_name": "placeholder"
}
}
}
| no | | [create](#input\_create) | Determines whether resources will be created (affects all resources) | `bool` | `true` | no | | [create\_cloudwatch\_log\_group](#input\_create\_cloudwatch\_log\_group) | Determines whether a log group is created by this module for the cluster logs. If not, AWS will automatically create one if logging is enabled | `bool` | `true` | no | +| [create\_infrastructure\_iam\_role](#input\_create\_infrastructure\_iam\_role) | Determines whether the ECS infrastructure IAM role should be created | `bool` | `true` | no | +| [create\_node\_iam\_instance\_profile](#input\_create\_node\_iam\_instance\_profile) | Determines whether an IAM instance profile is created or to use an existing IAM instance profile | `bool` | `true` | no | | [create\_task\_exec\_iam\_role](#input\_create\_task\_exec\_iam\_role) | Determines whether the ECS task definition IAM role should be created | `bool` | `false` | no | | [create\_task\_exec\_policy](#input\_create\_task\_exec\_policy) | Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters | `bool` | `true` | no | | [default\_capacity\_provider\_strategy](#input\_default\_capacity\_provider\_strategy) | Map of default capacity provider strategy definitions to use for the cluster |
map(object({
base = optional(number)
name = optional(string) # Will fall back to use map key if not set
weight = optional(number)
}))
| `{}` | no | +| [infrastructure\_iam\_role\_description](#input\_infrastructure\_iam\_role\_description) | Description of the role | `string` | `null` | no | +| [infrastructure\_iam\_role\_name](#input\_infrastructure\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | +| [infrastructure\_iam\_role\_path](#input\_infrastructure\_iam\_role\_path) | IAM role path | `string` | `null` | no | +| [infrastructure\_iam\_role\_permissions\_boundary](#input\_infrastructure\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [infrastructure\_iam\_role\_tags](#input\_infrastructure\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | +| [infrastructure\_iam\_role\_use\_name\_prefix](#input\_infrastructure\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`iam_role_name`) is used as a prefix | `bool` | `true` | no | | [name](#input\_name) | Name of the cluster (up to 255 letters, numbers, hyphens, and underscores) | `string` | `""` | no | +| [node\_iam\_role\_additional\_policies](#input\_node\_iam\_role\_additional\_policies) | Additional policies to be added to the IAM role | `map(string)` | `{}` | no | +| [node\_iam\_role\_description](#input\_node\_iam\_role\_description) | Description of the role | `string` | `"ECS Managed Instances node IAM role"` | no | +| [node\_iam\_role\_name](#input\_node\_iam\_role\_name) | Name to use on IAM role/instance profile created | `string` | `null` | no | +| [node\_iam\_role\_override\_policy\_documents](#input\_node\_iam\_role\_override\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid` | `list(string)` | `[]` | no | +| [node\_iam\_role\_path](#input\_node\_iam\_role\_path) | IAM role/instance profile path | `string` | `null` | no | +| [node\_iam\_role\_permissions\_boundary](#input\_node\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [node\_iam\_role\_source\_policy\_documents](#input\_node\_iam\_role\_source\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. Statements must have unique `sid`s | `list(string)` | `[]` | no | +| [node\_iam\_role\_statements](#input\_node\_iam\_role\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage |
map(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string, "Allow")
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
variable = string
values = list(string)
})))
}))
| `null` | no | +| [node\_iam\_role\_tags](#input\_node\_iam\_role\_tags) | A map of additional tags to add to the IAM role/instance profile created | `map(string)` | `{}` | no | +| [node\_iam\_role\_use\_name\_prefix](#input\_node\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role/instance profile name (`node_iam_role_name`) is used as a prefix | `bool` | `true` | no | | [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | | [service\_connect\_defaults](#input\_service\_connect\_defaults) | Configures a default Service Connect namespace |
object({
namespace = string
})
| `null` | no | | [setting](#input\_setting) | List of configuration block(s) with cluster settings. For example, this can be used to enable CloudWatch Container Insights for a cluster |
list(object({
name = string
value = string
}))
|
[
{
"name": "containerInsights",
"value": "enabled"
}
]
| no | @@ -205,10 +236,19 @@ No modules. | [cloudwatch\_log\_group\_name](#output\_cloudwatch\_log\_group\_name) | Name of CloudWatch log group created | | [cluster\_capacity\_providers](#output\_cluster\_capacity\_providers) | Map of cluster capacity providers attributes | | [id](#output\_id) | ID that identifies the cluster | +| [infrastructure\_iam\_role\_arn](#output\_infrastructure\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [infrastructure\_iam\_role\_name](#output\_infrastructure\_iam\_role\_name) | IAM role name | +| [infrastructure\_iam\_role\_unique\_id](#output\_infrastructure\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | | [name](#output\_name) | Name that identifies the cluster | -| [task\_exec\_iam\_role\_arn](#output\_task\_exec\_iam\_role\_arn) | Task execution IAM role ARN | -| [task\_exec\_iam\_role\_name](#output\_task\_exec\_iam\_role\_name) | Task execution IAM role name | -| [task\_exec\_iam\_role\_unique\_id](#output\_task\_exec\_iam\_role\_unique\_id) | Stable and unique string identifying the task execution IAM role | +| [node\_iam\_instance\_profile\_arn](#output\_node\_iam\_instance\_profile\_arn) | ARN assigned by AWS to the instance profile | +| [node\_iam\_instance\_profile\_id](#output\_node\_iam\_instance\_profile\_id) | Instance profile's ID | +| [node\_iam\_instance\_profile\_unique](#output\_node\_iam\_instance\_profile\_unique) | Stable and unique string identifying the IAM instance profile | +| [node\_iam\_role\_arn](#output\_node\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [node\_iam\_role\_name](#output\_node\_iam\_role\_name) | IAM role name | +| [node\_iam\_role\_unique\_id](#output\_node\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | +| [task\_exec\_iam\_role\_arn](#output\_task\_exec\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [task\_exec\_iam\_role\_name](#output\_task\_exec\_iam\_role\_name) | IAM role name | +| [task\_exec\_iam\_role\_unique\_id](#output\_task\_exec\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | ## License diff --git a/modules/cluster/main.tf b/modules/cluster/main.tf index 4ef0901b..72182dd3 100644 --- a/modules/cluster/main.tf +++ b/modules/cluster/main.tf @@ -1,3 +1,21 @@ +data "aws_region" "current" { + region = var.region + + count = var.create ? 1 : 0 +} +data "aws_partition" "current" { + count = var.create ? 1 : 0 +} +data "aws_caller_identity" "current" { + count = var.create ? 1 : 0 +} + +locals { + account_id = try(data.aws_caller_identity.current[0].account_id, "") + partition = try(data.aws_partition.current[0].partition, "") + region = try(data.aws_region.current[0].region, "") +} + ################################################################################ # Cluster ################################################################################ @@ -135,7 +153,7 @@ locals { autoscaling_group_provider = { autoscaling_group_arn = v.autoscaling_group_arn managed_draining = try(v.managed_draining, null) - managed_scaling = try(v.managed_draining, null) != null ? { + managed_scaling = try(v.managed_scaling, null) != null ? { instance_warmup_period = try(v.managed_scaling.instance_warmup_period, null) maximum_scaling_step_size = try(v.managed_scaling.maximum_scaling_step_size, null) minimum_scaling_step_size = try(v.managed_scaling.minimum_scaling_step_size, null) @@ -184,13 +202,13 @@ resource "aws_ecs_capacity_provider" "this" { for_each = each.value.managed_instances_provider != null ? [each.value.managed_instances_provider] : [] content { - infrastructure_role_arn = managed_instances_provider.value.infrastructure_role_arn + infrastructure_role_arn = local.create_infrastructure_iam_role ? aws_iam_role.infrastructure[0].arn : managed_instances_provider.value.infrastructure_role_arn dynamic "instance_launch_template" { for_each = managed_instances_provider.value.instance_launch_template != null ? [managed_instances_provider.value.instance_launch_template] : [] content { - ec2_instance_profile_arn = instance_launch_template.value.ec2_instance_profile_arn + ec2_instance_profile_arn = local.create_node_iam_instance_profile ? aws_iam_instance_profile.this[0].arn : instance_launch_template.value.ec2_instance_profile_arn dynamic "instance_requirements" { for_each = instance_launch_template.value.instance_requirements != null ? [instance_launch_template.value.instance_requirements] : [] @@ -323,7 +341,7 @@ resource "aws_ecs_capacity_provider" "this" { } } - cluster = each.value.auto_scaling_group_provider == null ? aws_ecs_cluster.this[0].id : null + cluster = each.value.managed_instances_provider != null ? aws_ecs_cluster.this[0].id : null name = try(coalesce(each.value.name, each.key), "") @@ -485,3 +503,239 @@ resource "aws_iam_role_policy_attachment" "task_exec" { role = aws_iam_role.task_exec[0].name policy_arn = aws_iam_policy.task_exec[0].arn } + +############################################################################################ +# Infrastructure IAM role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/infrastructure_IAM_role.html +############################################################################################ + +locals { + needs_infrastructure_iam_role = anytrue([for k, v in local.capacity_providers : v.managed_instances_provider != null]) + create_infrastructure_iam_role = var.create && var.create_infrastructure_iam_role && local.needs_infrastructure_iam_role + infrastructure_iam_role_name = coalesce(var.infrastructure_iam_role_name, "${var.name}-infra", "NotProvided") +} + +data "aws_iam_policy_document" "infrastructure" { + count = local.create_infrastructure_iam_role ? 1 : 0 + + statement { + sid = "ECSServiceAssumeRole" + actions = [ + "sts:AssumeRole", + "sts:TagSession", + ] + + principals { + type = "Service" + identifiers = ["ecs.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "infrastructure" { + count = local.create_infrastructure_iam_role ? 1 : 0 + + name = var.infrastructure_iam_role_use_name_prefix ? null : local.infrastructure_iam_role_name + name_prefix = var.infrastructure_iam_role_use_name_prefix ? "${local.infrastructure_iam_role_name}-" : null + path = var.infrastructure_iam_role_path + description = coalesce(var.infrastructure_iam_role_description, "Amazon ECS infrastructure IAM role that is used to manage your infrastructure (managed instances)") + + assume_role_policy = data.aws_iam_policy_document.infrastructure[0].json + permissions_boundary = var.infrastructure_iam_role_permissions_boundary + force_detach_policies = true + + tags = merge(var.tags, var.infrastructure_iam_role_tags) +} + +# https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonECSInfrastructureRolePolicyForManagedInstances.html +resource "aws_iam_role_policy_attachment" "infrastructure_managed_instances" { + count = local.create_infrastructure_iam_role ? 1 : 0 + + role = aws_iam_role.infrastructure[0].name + policy_arn = "arn:${local.partition}:iam::aws:policy/AmazonECSInfrastructureRolePolicyForManagedInstances" +} + +################################################################################ +# Node IAM role +################################################################################ + +locals { + create_node_iam_instance_profile = var.create && var.create_node_iam_instance_profile + + node_iam_role_name = coalesce(var.node_iam_role_name, "${var.name}-node") +} + +data "aws_iam_policy_document" "node_assume_role_policy" { + count = local.create_node_iam_instance_profile ? 1 : 0 + + statement { + sid = "ECSNodeAssumeRole" + actions = [ + "sts:AssumeRole", + "sts:TagSession", + ] + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "node" { + count = local.create_node_iam_instance_profile ? 1 : 0 + + name = var.node_iam_role_use_name_prefix ? null : local.node_iam_role_name + name_prefix = var.node_iam_role_use_name_prefix ? "${local.node_iam_role_name}-" : null + path = var.node_iam_role_path + description = var.node_iam_role_description + + assume_role_policy = data.aws_iam_policy_document.node_assume_role_policy[0].json + permissions_boundary = var.node_iam_role_permissions_boundary + force_detach_policies = true + + tags = merge(var.tags, var.node_iam_role_tags) +} + +resource "aws_iam_role_policy_attachment" "node_additional" { + for_each = { for k, v in var.node_iam_role_additional_policies : k => v if local.create_node_iam_instance_profile } + + policy_arn = each.value + role = aws_iam_role.node[0].name +} + +################################################################################ +# Node IAM role policy +# +# Due to this warning from ECS documentation +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/managed-instances-instance-profile.html +# +# > If you are using Amazon ECS Managed Instances with the AWS-managed Infrastructure policy, +# > the instance profile must be named ecsInstanceRole. If you are using a custom policy for +# > the Infrastructure role, the instance profile can have an alternative name. +# +# We default to creating the policy in order to remove this "surprising" requirement +# Ref: docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonECSInstanceRolePolicyForManagedInstances.html +################################################################################ + +data "aws_iam_policy_document" "node" { + count = local.create_node_iam_instance_profile ? 1 : 0 + + source_policy_documents = var.node_iam_role_source_policy_documents + override_policy_documents = var.node_iam_role_override_policy_documents + + statement { + sid = "ECSAgentDiscoverPollEndpointPermissions" + actions = ["ecs:DiscoverPollEndpoint"] + resources = ["*"] + } + + statement { + sid = "ECSAgentRegisterPermissions" + actions = ["ecs:RegisterContainerInstance"] + resources = [aws_ecs_cluster.this[0].arn] + } + + statement { + sid = "ECSAgentPollPermissions" + actions = ["ecs:Poll"] + resources = ["arn:${local.partition}:ecs:${local.region}:${local.account_id}:container-instance/*"] + } + + statement { + sid = "ECSAgentTelemetryPermissions" + actions = [ + "ecs:StartTelemetrySession", + "ecs:PutSystemLogEvents", + ] + resources = ["arn:${local.partition}:ecs:${local.region}:${local.account_id}:container-instance/*"] + } + + statement { + sid = "ECSAgentStateChangePermissions" + actions = [ + "ecs:SubmitAttachmentStateChanges", + "ecs:SubmitTaskStateChange", + ] + resources = [aws_ecs_cluster.this[0].arn] + } + + dynamic "statement" { + for_each = var.node_iam_role_statements != null ? var.node_iam_role_statements : {} + + content { + sid = try(coalesce(statement.value.sid, statement.key)) + actions = statement.value.actions + not_actions = statement.value.not_actions + effect = statement.value.effect + resources = statement.value.resources + not_resources = statement.value.not_resources + + dynamic "principals" { + for_each = statement.value.principals != null ? statement.value.principals : [] + + content { + type = principals.value.type + identifiers = principals.value.identifiers + } + } + + dynamic "not_principals" { + for_each = statement.value.not_principals != null ? statement.value.not_principals : [] + + content { + type = not_principals.value.type + identifiers = not_principals.value.identifiers + } + } + + dynamic "condition" { + for_each = statement.value.condition != null ? statement.value.condition : [] + + content { + test = condition.value.test + values = condition.value.values + variable = condition.value.variable + } + } + } + } +} + +resource "aws_iam_policy" "node" { + count = local.create_node_iam_instance_profile ? 1 : 0 + + name = var.node_iam_role_use_name_prefix ? null : local.node_iam_role_name + name_prefix = var.node_iam_role_use_name_prefix ? "${local.node_iam_role_name}-" : null + description = coalesce(var.node_iam_role_description, "ECS Managed Instances permissions") + policy = data.aws_iam_policy_document.node[0].json + + tags = merge(var.tags, var.node_iam_role_tags) +} + +resource "aws_iam_role_policy_attachment" "node" { + count = local.create_node_iam_instance_profile ? 1 : 0 + + policy_arn = aws_iam_policy.node[0].arn + role = aws_iam_role.node[0].name +} + +################################################################################ +# Node Instance Profile +################################################################################ + +resource "aws_iam_instance_profile" "this" { + count = local.create_node_iam_instance_profile ? 1 : 0 + + role = aws_iam_role.node[0].name + + name = var.node_iam_role_use_name_prefix ? null : local.node_iam_role_name + name_prefix = var.node_iam_role_use_name_prefix ? "${local.node_iam_role_name}-" : null + path = var.node_iam_role_path + + tags = merge(var.tags, var.node_iam_role_tags) + + lifecycle { + create_before_destroy = true + } +} diff --git a/modules/cluster/outputs.tf b/modules/cluster/outputs.tf index 4e05381f..ff100d27 100644 --- a/modules/cluster/outputs.tf +++ b/modules/cluster/outputs.tf @@ -54,17 +54,74 @@ output "autoscaling_capacity_providers" { # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html ################################################################################ -output "task_exec_iam_role_name" { - description = "Task execution IAM role name" - value = try(aws_iam_role.task_exec[0].name, null) -} - output "task_exec_iam_role_arn" { - description = "Task execution IAM role ARN" + description = "The Amazon Resource Name (ARN) specifying the IAM role" value = try(aws_iam_role.task_exec[0].arn, null) } +output "task_exec_iam_role_name" { + description = "IAM role name" + value = try(aws_iam_role.task_exec[0].name, null) +} + output "task_exec_iam_role_unique_id" { - description = "Stable and unique string identifying the task execution IAM role" + description = "Stable and unique string identifying the IAM role" value = try(aws_iam_role.task_exec[0].unique_id, null) } + +############################################################################################ +# Infrastructure IAM role +############################################################################################ + +output "infrastructure_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = try(aws_iam_role.infrastructure[0].arn, null) +} + +output "infrastructure_iam_role_name" { + description = "IAM role name" + value = try(aws_iam_role.infrastructure[0].name, null) +} + +output "infrastructure_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = try(aws_iam_role.infrastructure[0].unique_id, null) +} + +################################################################################ +# Node IAM role +################################################################################ + +output "node_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = try(aws_iam_role.node[0].arn, null) +} + +output "node_iam_role_name" { + description = "IAM role name" + value = try(aws_iam_role.node[0].name, null) +} + +output "node_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = try(aws_iam_role.node[0].unique_id, null) +} + +################################################################################ +# IAM Instance Profile +################################################################################ + +output "node_iam_instance_profile_arn" { + description = "ARN assigned by AWS to the instance profile" + value = try(aws_iam_instance_profile.this[0].arn, null) +} + +output "node_iam_instance_profile_id" { + description = "Instance profile's ID" + value = try(aws_iam_instance_profile.this[0].id, null) +} + +output "node_iam_instance_profile_unique" { + description = "Stable and unique string identifying the IAM instance profile" + value = try(aws_iam_instance_profile.this[0].unique_id, null) +} diff --git a/modules/cluster/variables.tf b/modules/cluster/variables.tf index 4dac0c4b..00a372e1 100644 --- a/modules/cluster/variables.tf +++ b/modules/cluster/variables.tf @@ -336,3 +336,151 @@ variable "task_exec_iam_statements" { })) default = null } + +############################################################################################ +# Infrastructure IAM role +############################################################################################ + +variable "create_infrastructure_iam_role" { + description = "Determines whether the ECS infrastructure IAM role should be created" + type = bool + default = true + nullable = false +} + +variable "infrastructure_iam_role_name" { + description = "Name to use on IAM role created" + type = string + default = null +} + +variable "infrastructure_iam_role_use_name_prefix" { + description = "Determines whether the IAM role name (`iam_role_name`) is used as a prefix" + type = bool + default = true + nullable = false +} + +variable "infrastructure_iam_role_path" { + description = "IAM role path" + type = string + default = null +} + +variable "infrastructure_iam_role_description" { + description = "Description of the role" + type = string + default = null +} + +variable "infrastructure_iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "infrastructure_iam_role_tags" { + description = "A map of additional tags to add to the IAM role created" + type = map(string) + default = {} + nullable = false +} + +################################################################################ +# Node IAM role & instance profile +################################################################################ + +variable "create_node_iam_instance_profile" { + description = "Determines whether an IAM instance profile is created or to use an existing IAM instance profile" + type = bool + default = true + nullable = false +} + +variable "node_iam_role_name" { + description = "Name to use on IAM role/instance profile created" + type = string + default = null +} + +variable "node_iam_role_use_name_prefix" { + description = "Determines whether the IAM role/instance profile name (`node_iam_role_name`) is used as a prefix" + type = bool + default = true + nullable = false +} + +variable "node_iam_role_path" { + description = "IAM role/instance profile path" + type = string + default = null +} + +variable "node_iam_role_description" { + description = "Description of the role" + type = string + default = "ECS Managed Instances node IAM role" + nullable = false +} + +variable "node_iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "node_iam_role_additional_policies" { + description = "Additional policies to be added to the IAM role" + type = map(string) + default = {} + nullable = false +} + +variable "node_iam_role_tags" { + description = "A map of additional tags to add to the IAM role/instance profile created" + type = map(string) + default = {} + nullable = false +} + +################################################################################ +# Node IAM role policy +################################################################################ + +variable "node_iam_role_source_policy_documents" { + description = "List of IAM policy documents that are merged together into the exported document. Statements must have unique `sid`s" + type = list(string) + default = [] +} + +variable "node_iam_role_override_policy_documents" { + description = "List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid`" + type = list(string) + default = [] +} + +variable "node_iam_role_statements" { + description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" + type = map(object({ + sid = optional(string) + actions = optional(list(string)) + not_actions = optional(list(string)) + effect = optional(string, "Allow") + resources = optional(list(string)) + not_resources = optional(list(string)) + principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + not_principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + condition = optional(list(object({ + test = string + variable = string + values = list(string) + }))) + })) + default = null +} diff --git a/modules/service/README.md b/modules/service/README.md index f0940eb1..75203644 100644 --- a/modules/service/README.md +++ b/modules/service/README.md @@ -295,7 +295,7 @@ module "ecs_service" { | [propagate\_tags](#input\_propagate\_tags) | Specifies whether to propagate the tags from the task definition or the service to the tasks. The valid values are `SERVICE` and `TASK_DEFINITION` | `string` | `null` | no | | [proxy\_configuration](#input\_proxy\_configuration) | Configuration block for the App Mesh proxy |
object({
container_name = string
properties = optional(map(string))
type = optional(string)
})
| `null` | no | | [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | -| [requires\_compatibilities](#input\_requires\_compatibilities) | Set of launch types required by the task. The valid values are `EC2` and `FARGATE` | `list(string)` |
[
"FARGATE"
]
| no | +| [requires\_compatibilities](#input\_requires\_compatibilities) | Set of launch types required by the task. The valid values are `EC2`, `FARGATE`, `EXTERNAL`, and `MANAGED_INSTANCES` | `list(string)` |
[
"FARGATE"
]
| no | | [runtime\_platform](#input\_runtime\_platform) | Configuration block for `runtime_platform` that containers in your task may use |
object({
cpu_architecture = optional(string, "X86_64")
operating_system_family = optional(string, "LINUX")
})
|
{
"cpu_architecture": "X86_64",
"operating_system_family": "LINUX"
}
| no | | [scale](#input\_scale) | A floating-point percentage of the desired number of tasks to place and keep running in the task set |
object({
unit = optional(string)
value = optional(number)
})
| `null` | no | | [scheduling\_strategy](#input\_scheduling\_strategy) | Scheduling strategy to use for the service. The valid values are `REPLICA` and `DAEMON`. Defaults to `REPLICA` | `string` | `null` | no | diff --git a/modules/service/variables.tf b/modules/service/variables.tf index b42dfe40..c01c75cb 100644 --- a/modules/service/variables.tf +++ b/modules/service/variables.tf @@ -683,7 +683,7 @@ variable "proxy_configuration" { } variable "requires_compatibilities" { - description = "Set of launch types required by the task. The valid values are `EC2` and `FARGATE`" + description = "Set of launch types required by the task. The valid values are `EC2`, `FARGATE`, `EXTERNAL`, and `MANAGED_INSTANCES`" type = list(string) default = ["FARGATE"] nullable = false @@ -1292,7 +1292,7 @@ variable "security_group_tags" { } ############################################################################################ -# ECS Infrastructure IAM role +# Infrastructure IAM role ############################################################################################ variable "create_infrastructure_iam_role" { diff --git a/outputs.tf b/outputs.tf index 6e462c6e..ae9d9d35 100644 --- a/outputs.tf +++ b/outputs.tf @@ -52,6 +52,51 @@ output "task_exec_iam_role_unique_id" { value = module.cluster.task_exec_iam_role_unique_id } +output "infrastructure_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = module.cluster.infrastructure_iam_role_arn +} + +output "infrastructure_iam_role_name" { + description = "IAM role name" + value = module.cluster.infrastructure_iam_role_name +} + +output "infrastructure_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = module.cluster.infrastructure_iam_role_unique_id +} + +output "node_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = module.cluster.node_iam_role_arn +} + +output "node_iam_role_name" { + description = "IAM role name" + value = module.cluster.node_iam_role_name +} + +output "node_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = module.cluster.node_iam_role_unique_id +} + +output "node_iam_instance_profile_arn" { + description = "ARN assigned by AWS to the instance profile" + value = module.cluster.node_iam_instance_profile_arn +} + +output "node_iam_instance_profile_id" { + description = "Instance profile's ID" + value = module.cluster.node_iam_instance_profile_id +} + +output "node_iam_instance_profile_unique" { + description = "Stable and unique string identifying the IAM instance profile" + value = module.cluster.node_iam_instance_profile_unique +} + ################################################################################ # Service(s) ################################################################################ diff --git a/variables.tf b/variables.tf index 26bf1671..fa583d8a 100644 --- a/variables.tf +++ b/variables.tf @@ -341,6 +341,154 @@ variable "task_exec_iam_statements" { default = null } +############################################################################################ +# Infrastructure IAM role +############################################################################################ + +variable "create_infrastructure_iam_role" { + description = "Determines whether the ECS infrastructure IAM role should be created" + type = bool + default = true + nullable = false +} + +variable "infrastructure_iam_role_name" { + description = "Name to use on IAM role created" + type = string + default = null +} + +variable "infrastructure_iam_role_use_name_prefix" { + description = "Determines whether the IAM role name (`iam_role_name`) is used as a prefix" + type = bool + default = true + nullable = false +} + +variable "infrastructure_iam_role_path" { + description = "IAM role path" + type = string + default = null +} + +variable "infrastructure_iam_role_description" { + description = "Description of the role" + type = string + default = null +} + +variable "infrastructure_iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "infrastructure_iam_role_tags" { + description = "A map of additional tags to add to the IAM role created" + type = map(string) + default = {} + nullable = false +} + +################################################################################ +# Node IAM role & instance profile +################################################################################ + +variable "create_node_iam_instance_profile" { + description = "Determines whether an IAM instance profile is created or to use an existing IAM instance profile" + type = bool + default = true + nullable = false +} + +variable "node_iam_role_name" { + description = "Name to use on IAM role/instance profile created" + type = string + default = null +} + +variable "node_iam_role_use_name_prefix" { + description = "Determines whether the IAM role/instance profile name (`node_iam_role_name`) is used as a prefix" + type = bool + default = true + nullable = false +} + +variable "node_iam_role_path" { + description = "IAM role/instance profile path" + type = string + default = null +} + +variable "node_iam_role_description" { + description = "Description of the role" + type = string + default = "ECS Managed Instances node IAM role" + nullable = false +} + +variable "node_iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "node_iam_role_additional_policies" { + description = "Additional policies to be added to the IAM role" + type = map(string) + default = {} + nullable = false +} + +variable "node_iam_role_tags" { + description = "A map of additional tags to add to the IAM role/instance profile created" + type = map(string) + default = {} + nullable = false +} + +################################################################################ +# Node IAM role policy +################################################################################ + +variable "node_iam_role_source_policy_documents" { + description = "List of IAM policy documents that are merged together into the exported document. Statements must have unique `sid`s" + type = list(string) + default = [] +} + +variable "node_iam_role_override_policy_documents" { + description = "List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid`" + type = list(string) + default = [] +} + +variable "node_iam_role_statements" { + description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" + type = map(object({ + sid = optional(string) + actions = optional(list(string)) + not_actions = optional(list(string)) + effect = optional(string, "Allow") + resources = optional(list(string)) + not_resources = optional(list(string)) + principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + not_principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + condition = optional(list(object({ + test = string + variable = string + values = list(string) + }))) + })) + default = null +} + ################################################################################ # Service(s) ################################################################################ diff --git a/wrappers/cluster/main.tf b/wrappers/cluster/main.tf index 70590835..273131a1 100644 --- a/wrappers/cluster/main.tf +++ b/wrappers/cluster/main.tf @@ -17,14 +17,32 @@ module "wrapper" { } } }) - create = try(each.value.create, var.defaults.create, true) - create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.defaults.create_cloudwatch_log_group, true) - create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, false) - create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) - default_capacity_provider_strategy = try(each.value.default_capacity_provider_strategy, var.defaults.default_capacity_provider_strategy, {}) - name = try(each.value.name, var.defaults.name, "") - region = try(each.value.region, var.defaults.region, null) - service_connect_defaults = try(each.value.service_connect_defaults, var.defaults.service_connect_defaults, null) + create = try(each.value.create, var.defaults.create, true) + create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.defaults.create_cloudwatch_log_group, true) + create_infrastructure_iam_role = try(each.value.create_infrastructure_iam_role, var.defaults.create_infrastructure_iam_role, true) + create_node_iam_instance_profile = try(each.value.create_node_iam_instance_profile, var.defaults.create_node_iam_instance_profile, true) + create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, false) + create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) + default_capacity_provider_strategy = try(each.value.default_capacity_provider_strategy, var.defaults.default_capacity_provider_strategy, {}) + infrastructure_iam_role_description = try(each.value.infrastructure_iam_role_description, var.defaults.infrastructure_iam_role_description, null) + infrastructure_iam_role_name = try(each.value.infrastructure_iam_role_name, var.defaults.infrastructure_iam_role_name, null) + infrastructure_iam_role_path = try(each.value.infrastructure_iam_role_path, var.defaults.infrastructure_iam_role_path, null) + infrastructure_iam_role_permissions_boundary = try(each.value.infrastructure_iam_role_permissions_boundary, var.defaults.infrastructure_iam_role_permissions_boundary, null) + infrastructure_iam_role_tags = try(each.value.infrastructure_iam_role_tags, var.defaults.infrastructure_iam_role_tags, {}) + infrastructure_iam_role_use_name_prefix = try(each.value.infrastructure_iam_role_use_name_prefix, var.defaults.infrastructure_iam_role_use_name_prefix, true) + name = try(each.value.name, var.defaults.name, "") + node_iam_role_additional_policies = try(each.value.node_iam_role_additional_policies, var.defaults.node_iam_role_additional_policies, {}) + node_iam_role_description = try(each.value.node_iam_role_description, var.defaults.node_iam_role_description, "ECS Managed Instances node IAM role") + node_iam_role_name = try(each.value.node_iam_role_name, var.defaults.node_iam_role_name, null) + node_iam_role_override_policy_documents = try(each.value.node_iam_role_override_policy_documents, var.defaults.node_iam_role_override_policy_documents, []) + node_iam_role_path = try(each.value.node_iam_role_path, var.defaults.node_iam_role_path, null) + node_iam_role_permissions_boundary = try(each.value.node_iam_role_permissions_boundary, var.defaults.node_iam_role_permissions_boundary, null) + node_iam_role_source_policy_documents = try(each.value.node_iam_role_source_policy_documents, var.defaults.node_iam_role_source_policy_documents, []) + node_iam_role_statements = try(each.value.node_iam_role_statements, var.defaults.node_iam_role_statements, null) + node_iam_role_tags = try(each.value.node_iam_role_tags, var.defaults.node_iam_role_tags, {}) + node_iam_role_use_name_prefix = try(each.value.node_iam_role_use_name_prefix, var.defaults.node_iam_role_use_name_prefix, true) + region = try(each.value.region, var.defaults.region, null) + service_connect_defaults = try(each.value.service_connect_defaults, var.defaults.service_connect_defaults, null) setting = try(each.value.setting, var.defaults.setting, [ { name = "containerInsights" diff --git a/wrappers/main.tf b/wrappers/main.tf index 0c27c2a8..eee052c7 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -25,23 +25,41 @@ module "wrapper" { value = "enabled" } ]) - cluster_tags = try(each.value.cluster_tags, var.defaults.cluster_tags, {}) - create = try(each.value.create, var.defaults.create, true) - create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.defaults.create_cloudwatch_log_group, true) - create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, false) - create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) - default_capacity_provider_strategy = try(each.value.default_capacity_provider_strategy, var.defaults.default_capacity_provider_strategy, null) - region = try(each.value.region, var.defaults.region, null) - services = try(each.value.services, var.defaults.services, null) - tags = try(each.value.tags, var.defaults.tags, {}) - task_exec_iam_role_description = try(each.value.task_exec_iam_role_description, var.defaults.task_exec_iam_role_description, null) - task_exec_iam_role_name = try(each.value.task_exec_iam_role_name, var.defaults.task_exec_iam_role_name, null) - task_exec_iam_role_path = try(each.value.task_exec_iam_role_path, var.defaults.task_exec_iam_role_path, null) - task_exec_iam_role_permissions_boundary = try(each.value.task_exec_iam_role_permissions_boundary, var.defaults.task_exec_iam_role_permissions_boundary, null) - task_exec_iam_role_policies = try(each.value.task_exec_iam_role_policies, var.defaults.task_exec_iam_role_policies, {}) - task_exec_iam_role_tags = try(each.value.task_exec_iam_role_tags, var.defaults.task_exec_iam_role_tags, {}) - task_exec_iam_role_use_name_prefix = try(each.value.task_exec_iam_role_use_name_prefix, var.defaults.task_exec_iam_role_use_name_prefix, true) - task_exec_iam_statements = try(each.value.task_exec_iam_statements, var.defaults.task_exec_iam_statements, null) - task_exec_secret_arns = try(each.value.task_exec_secret_arns, var.defaults.task_exec_secret_arns, []) - task_exec_ssm_param_arns = try(each.value.task_exec_ssm_param_arns, var.defaults.task_exec_ssm_param_arns, []) + cluster_tags = try(each.value.cluster_tags, var.defaults.cluster_tags, {}) + create = try(each.value.create, var.defaults.create, true) + create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.defaults.create_cloudwatch_log_group, true) + create_infrastructure_iam_role = try(each.value.create_infrastructure_iam_role, var.defaults.create_infrastructure_iam_role, true) + create_node_iam_instance_profile = try(each.value.create_node_iam_instance_profile, var.defaults.create_node_iam_instance_profile, true) + create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, false) + create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) + default_capacity_provider_strategy = try(each.value.default_capacity_provider_strategy, var.defaults.default_capacity_provider_strategy, null) + infrastructure_iam_role_description = try(each.value.infrastructure_iam_role_description, var.defaults.infrastructure_iam_role_description, null) + infrastructure_iam_role_name = try(each.value.infrastructure_iam_role_name, var.defaults.infrastructure_iam_role_name, null) + infrastructure_iam_role_path = try(each.value.infrastructure_iam_role_path, var.defaults.infrastructure_iam_role_path, null) + infrastructure_iam_role_permissions_boundary = try(each.value.infrastructure_iam_role_permissions_boundary, var.defaults.infrastructure_iam_role_permissions_boundary, null) + infrastructure_iam_role_tags = try(each.value.infrastructure_iam_role_tags, var.defaults.infrastructure_iam_role_tags, {}) + infrastructure_iam_role_use_name_prefix = try(each.value.infrastructure_iam_role_use_name_prefix, var.defaults.infrastructure_iam_role_use_name_prefix, true) + node_iam_role_additional_policies = try(each.value.node_iam_role_additional_policies, var.defaults.node_iam_role_additional_policies, {}) + node_iam_role_description = try(each.value.node_iam_role_description, var.defaults.node_iam_role_description, "ECS Managed Instances node IAM role") + node_iam_role_name = try(each.value.node_iam_role_name, var.defaults.node_iam_role_name, null) + node_iam_role_override_policy_documents = try(each.value.node_iam_role_override_policy_documents, var.defaults.node_iam_role_override_policy_documents, []) + node_iam_role_path = try(each.value.node_iam_role_path, var.defaults.node_iam_role_path, null) + node_iam_role_permissions_boundary = try(each.value.node_iam_role_permissions_boundary, var.defaults.node_iam_role_permissions_boundary, null) + node_iam_role_source_policy_documents = try(each.value.node_iam_role_source_policy_documents, var.defaults.node_iam_role_source_policy_documents, []) + node_iam_role_statements = try(each.value.node_iam_role_statements, var.defaults.node_iam_role_statements, null) + node_iam_role_tags = try(each.value.node_iam_role_tags, var.defaults.node_iam_role_tags, {}) + node_iam_role_use_name_prefix = try(each.value.node_iam_role_use_name_prefix, var.defaults.node_iam_role_use_name_prefix, true) + region = try(each.value.region, var.defaults.region, null) + services = try(each.value.services, var.defaults.services, null) + tags = try(each.value.tags, var.defaults.tags, {}) + task_exec_iam_role_description = try(each.value.task_exec_iam_role_description, var.defaults.task_exec_iam_role_description, null) + task_exec_iam_role_name = try(each.value.task_exec_iam_role_name, var.defaults.task_exec_iam_role_name, null) + task_exec_iam_role_path = try(each.value.task_exec_iam_role_path, var.defaults.task_exec_iam_role_path, null) + task_exec_iam_role_permissions_boundary = try(each.value.task_exec_iam_role_permissions_boundary, var.defaults.task_exec_iam_role_permissions_boundary, null) + task_exec_iam_role_policies = try(each.value.task_exec_iam_role_policies, var.defaults.task_exec_iam_role_policies, {}) + task_exec_iam_role_tags = try(each.value.task_exec_iam_role_tags, var.defaults.task_exec_iam_role_tags, {}) + task_exec_iam_role_use_name_prefix = try(each.value.task_exec_iam_role_use_name_prefix, var.defaults.task_exec_iam_role_use_name_prefix, true) + task_exec_iam_statements = try(each.value.task_exec_iam_statements, var.defaults.task_exec_iam_statements, null) + task_exec_secret_arns = try(each.value.task_exec_secret_arns, var.defaults.task_exec_secret_arns, []) + task_exec_ssm_param_arns = try(each.value.task_exec_ssm_param_arns, var.defaults.task_exec_ssm_param_arns, []) } From dd09227adbb2554c664ae9a074531fd4c325c683 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Thu, 23 Oct 2025 15:42:45 -0500 Subject: [PATCH 03/10] feat: Add support for creating security group for ECS Managed Instances --- modules/cluster/main.tf | 85 ++++++++++++++++++++++++++++++++++-- modules/cluster/variables.tf | 83 ++++++++++++++++++++++++++++++++++- variables.tf | 2 +- 3 files changed, 164 insertions(+), 6 deletions(-) diff --git a/modules/cluster/main.tf b/modules/cluster/main.tf index 72182dd3..c442e085 100644 --- a/modules/cluster/main.tf +++ b/modules/cluster/main.tf @@ -145,6 +145,8 @@ resource "aws_ecs_cluster_capacity_providers" "this" { ################################################################################ locals { + managed_instances_enabled = anytrue([for k, v in local.capacity_providers : v.managed_instances_provider != null]) + # TODO - embed the `autoscaling_capacity_providers` into a shape acceptable for # `var.capacity_providers` so that it can be merged with the new `capacity_providers` # for backward compatibility. Remove `autoscaling_capacity_providers` in the next major version. @@ -322,7 +324,7 @@ resource "aws_ecs_capacity_provider" "this" { for_each = instance_launch_template.value.network_configuration != null ? [instance_launch_template.value.network_configuration] : [] content { - security_groups = network_configuration.value.security_groups + security_groups = local.create_security_group ? flatten(concat(aws_security_group.this[*].id, network_configuration.value.security_groups)) : network_configuration.value.security_groups subnets = network_configuration.value.subnets } } @@ -510,8 +512,8 @@ resource "aws_iam_role_policy_attachment" "task_exec" { ############################################################################################ locals { - needs_infrastructure_iam_role = anytrue([for k, v in local.capacity_providers : v.managed_instances_provider != null]) - create_infrastructure_iam_role = var.create && var.create_infrastructure_iam_role && local.needs_infrastructure_iam_role + create_infrastructure_iam_role = var.create && var.create_infrastructure_iam_role && local.managed_instances_enabled + infrastructure_iam_role_name = coalesce(var.infrastructure_iam_role_name, "${var.name}-infra", "NotProvided") } @@ -560,7 +562,7 @@ resource "aws_iam_role_policy_attachment" "infrastructure_managed_instances" { ################################################################################ locals { - create_node_iam_instance_profile = var.create && var.create_node_iam_instance_profile + create_node_iam_instance_profile = var.create && var.create_node_iam_instance_profile && local.managed_instances_enabled node_iam_role_name = coalesce(var.node_iam_role_name, "${var.name}-node") } @@ -739,3 +741,78 @@ resource "aws_iam_instance_profile" "this" { create_before_destroy = true } } + +################################################################################ +# Security Group +################################################################################ + +locals { + create_security_group = var.create && var.create_security_group && local.managed_instances_enabled + + security_group_name = coalesce(var.security_group_name, var.name, "NotProvided") +} + +resource "aws_security_group" "this" { + count = local.create_security_group ? 1 : 0 + + region = var.region + + name = var.security_group_use_name_prefix ? null : local.security_group_name + name_prefix = var.security_group_use_name_prefix ? "${local.security_group_name}-" : null + description = var.security_group_description + vpc_id = var.vpc_id + + tags = merge( + var.tags, + { Name = local.security_group_name }, + var.security_group_tags + ) + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_vpc_security_group_ingress_rule" "this" { + for_each = { for k, v in var.security_group_ingress_rules : k => v if var.security_group_ingress_rules != null && local.create_security_group } + + region = var.region + + cidr_ipv4 = each.value.cidr_ipv4 + cidr_ipv6 = each.value.cidr_ipv6 + description = each.value.description + from_port = each.value.from_port + ip_protocol = each.value.ip_protocol + prefix_list_id = each.value.prefix_list_id + referenced_security_group_id = each.value.referenced_security_group_id == "self" ? aws_security_group.this[0].id : each.value.referenced_security_group_id + security_group_id = aws_security_group.this[0].id + tags = merge( + var.tags, + var.security_group_tags, + { "Name" = coalesce(each.value.name, "${local.security_group_name}-${each.key}") }, + each.value.tags + ) + to_port = try(coalesce(each.value.to_port, each.value.from_port), null) +} + +resource "aws_vpc_security_group_egress_rule" "this" { + for_each = { for k, v in var.security_group_egress_rules : k => v if var.security_group_egress_rules != null && local.create_security_group } + + region = var.region + + cidr_ipv4 = each.value.cidr_ipv4 + cidr_ipv6 = each.value.cidr_ipv6 + description = each.value.description + from_port = try(coalesce(each.value.from_port, each.value.to_port), null) + ip_protocol = each.value.ip_protocol + prefix_list_id = each.value.prefix_list_id + referenced_security_group_id = each.value.referenced_security_group_id == "self" ? aws_security_group.this[0].id : each.value.referenced_security_group_id + security_group_id = aws_security_group.this[0].id + tags = merge( + var.tags, + var.security_group_tags, + { "Name" = coalesce(each.value.name, "${local.security_group_name}-${each.key}") }, + each.value.tags + ) + to_port = each.value.to_port +} diff --git a/modules/cluster/variables.tf b/modules/cluster/variables.tf index 00a372e1..c1154202 100644 --- a/modules/cluster/variables.tf +++ b/modules/cluster/variables.tf @@ -214,7 +214,7 @@ variable "capacity_providers" { })) monitoring = optional(string) network_configuration = optional(object({ - security_groups = optional(list(string)) + security_groups = optional(list(string), []) subnets = list(string) })) storage_configuration = optional(object({ @@ -484,3 +484,84 @@ variable "node_iam_role_statements" { })) default = null } + +################################################################################ +# Security Group +################################################################################ + +variable "create_security_group" { + description = "Determines if a security group is created" + type = bool + default = true + nullable = false +} + +variable "vpc_id" { + description = "The ID of the VPC where the security group will be created" + type = string + default = null +} + +variable "security_group_name" { + description = "Name to use on security group created" + type = string + default = null +} + +variable "security_group_use_name_prefix" { + description = "Determines whether the security group name (`security_group_name`) is used as a prefix" + type = bool + default = true + nullable = false +} + +variable "security_group_description" { + description = "Description of the security group created" + type = string + default = null +} + +variable "security_group_ingress_rules" { + description = "Security group ingress rules to add to the security group created" + type = map(object({ + name = optional(string) + + cidr_ipv4 = optional(string) + cidr_ipv6 = optional(string) + description = optional(string) + from_port = optional(string) + ip_protocol = optional(string, "tcp") + prefix_list_id = optional(string) + referenced_security_group_id = optional(string) + tags = optional(map(string), {}) + to_port = optional(string) + })) + default = {} + nullable = false +} + +variable "security_group_egress_rules" { + description = "Security group egress rules to add to the security group created" + type = map(object({ + name = optional(string) + + cidr_ipv4 = optional(string) + cidr_ipv6 = optional(string) + description = optional(string) + from_port = optional(string) + ip_protocol = optional(string, "tcp") + prefix_list_id = optional(string) + referenced_security_group_id = optional(string) + tags = optional(map(string), {}) + to_port = optional(string) + })) + default = {} + nullable = false +} + +variable "security_group_tags" { + description = "A map of additional tags to add to the security group created" + type = map(string) + default = {} + nullable = false +} diff --git a/variables.tf b/variables.tf index fa583d8a..78caf5f1 100644 --- a/variables.tf +++ b/variables.tf @@ -219,7 +219,7 @@ variable "capacity_providers" { })) monitoring = optional(string) network_configuration = optional(object({ - security_groups = optional(list(string)) + security_groups = optional(list(string), []) subnets = list(string) })) storage_configuration = optional(object({ From 1b6c5892cecbd2bdafaa5a886d2250c5c251b3be Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Thu, 23 Oct 2025 18:57:18 -0500 Subject: [PATCH 04/10] feat: Add example for ECS Managed Instances --- README.md | 5 +- examples/complete/README.md | 22 ++- examples/complete/outputs.tf | 76 +++++++- examples/container-definition/README.md | 6 +- examples/ec2-autoscaling/README.md | 22 ++- examples/ec2-autoscaling/outputs.tf | 76 +++++++- examples/fargate/README.md | 22 ++- examples/fargate/outputs.tf | 76 +++++++- examples/managed-instances/README.md | 106 +++++++++++ examples/managed-instances/main.tf | 232 ++++++++++++++++++++++++ examples/managed-instances/outputs.tf | 217 ++++++++++++++++++++++ examples/managed-instances/variables.tf | 0 examples/managed-instances/versions.tf | 10 + modules/cluster/README.md | 16 +- modules/cluster/main.tf | 19 +- modules/cluster/outputs.tf | 5 + modules/cluster/variables.tf | 4 +- outputs.tf | 7 +- variables.tf | 4 +- wrappers/cluster/main.tf | 8 + 20 files changed, 894 insertions(+), 39 deletions(-) create mode 100644 examples/managed-instances/README.md create mode 100644 examples/managed-instances/main.tf create mode 100644 examples/managed-instances/outputs.tf create mode 100644 examples/managed-instances/variables.tf create mode 100644 examples/managed-instances/versions.tf diff --git a/README.md b/README.md index 29b7edfa..a4de671a 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ No resources. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [autoscaling\_capacity\_providers](#input\_autoscaling\_capacity\_providers) | [DEPRECATED - use `capacity_providers` instead] Map of autoscaling capacity provider definitions to create for the cluster |
map(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | -| [capacity\_providers](#input\_capacity\_providers) | Map of capacity provider definitions to create for the cluster |
map(object({
autoscaling_group_provider = optional(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
}))
managed_instances_provider = optional(object({
infrastructure_role_arn = optional(string)
instance_launch_template = object({
ec2_instance_profile_arn = optional(string)
instance_requirements = optional(object({
accelerator_count = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_manufacturers = optional(list(string))
accelerator_names = optional(list(string))
accelerator_total_memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_types = optional(list(string))
allowed_instance_types = optional(list(string))
bare_metal = optional(string)
baseline_ebs_bandwidth_mbps = optional(object({
max = optional(number)
min = optional(number)
}))
burstable_performance = optional(string)
cpu_manufacturers = optional(list(string))
excluded_instance_types = optional(list(string))
instance_generations = optional(list(string))
local_storage = optional(string)
local_storage_types = optional(list(string))
max_spot_price_as_percentage_of_optimal_on_demand_price = optional(number)
memory_gib_per_vcpu = optional(object({
max = optional(number)
min = optional(number)
}))
memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
network_bandwidth_gbps = optional(object({
max = optional(number)
min = optional(number)
}))
network_interface_count = optional(object({
max = optional(number)
min = optional(number)
}))
on_demand_max_price_percentage_over_lowest_price = optional(number)
require_hibernate_support = optional(bool)
spot_max_price_percentage_over_lowest_price = optional(number)
total_local_storage_gb = optional(object({
max = optional(number)
min = optional(number)
}))
vcpu_count = optional(object({
max = optional(number)
min = optional(number)
}))
}))
monitoring = optional(string)
network_configuration = optional(object({
security_groups = optional(list(string))
subnets = list(string)
}))
storage_configuration = optional(object({
storage_size_gib = number
}))
})
propagate_tags = optional(string)
}))
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | +| [capacity\_providers](#input\_capacity\_providers) | Map of capacity provider definitions to create for the cluster |
map(object({
auto_scaling_group_provider = optional(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
}))
managed_instances_provider = optional(object({
infrastructure_role_arn = optional(string)
instance_launch_template = object({
ec2_instance_profile_arn = optional(string)
instance_requirements = optional(object({
accelerator_count = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_manufacturers = optional(list(string))
accelerator_names = optional(list(string))
accelerator_total_memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_types = optional(list(string))
allowed_instance_types = optional(list(string))
bare_metal = optional(string)
baseline_ebs_bandwidth_mbps = optional(object({
max = optional(number)
min = optional(number)
}))
burstable_performance = optional(string)
cpu_manufacturers = optional(list(string))
excluded_instance_types = optional(list(string))
instance_generations = optional(list(string))
local_storage = optional(string)
local_storage_types = optional(list(string))
max_spot_price_as_percentage_of_optimal_on_demand_price = optional(number)
memory_gib_per_vcpu = optional(object({
max = optional(number)
min = optional(number)
}))
memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
network_bandwidth_gbps = optional(object({
max = optional(number)
min = optional(number)
}))
network_interface_count = optional(object({
max = optional(number)
min = optional(number)
}))
on_demand_max_price_percentage_over_lowest_price = optional(number)
require_hibernate_support = optional(bool)
spot_max_price_percentage_over_lowest_price = optional(number)
total_local_storage_gb = optional(object({
max = optional(number)
min = optional(number)
}))
vcpu_count = optional(object({
max = optional(number)
min = optional(number)
}))
}))
monitoring = optional(string)
network_configuration = optional(object({
security_groups = optional(list(string), [])
subnets = list(string)
}))
storage_configuration = optional(object({
storage_size_gib = number
}))
})
propagate_tags = optional(string, "CAPACITY_PROVIDER")
}))
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | | [cloudwatch\_log\_group\_class](#input\_cloudwatch\_log\_group\_class) | Specified the log class of the log group. Possible values are: `STANDARD` or `INFREQUENT_ACCESS` | `string` | `null` | no | | [cloudwatch\_log\_group\_kms\_key\_id](#input\_cloudwatch\_log\_group\_kms\_key\_id) | If a KMS Key ARN is set, this key will be used to encrypt the corresponding log group. Please be sure that the KMS Key has an appropriate key policy (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html) | `string` | `null` | no | | [cloudwatch\_log\_group\_name](#input\_cloudwatch\_log\_group\_name) | Custom name of CloudWatch Log Group for ECS cluster | `string` | `null` | no | @@ -230,7 +230,8 @@ No resources. | Name | Description | |------|-------------| -| [autoscaling\_capacity\_providers](#output\_autoscaling\_capacity\_providers) | Map of autoscaling capacity providers created and their attributes | +| [autoscaling\_capacity\_providers](#output\_autoscaling\_capacity\_providers) | [DEPRECATED - use `capacity_providers`] Map of autoscaling capacity providers created and their attributes | +| [capacity\_providers](#output\_capacity\_providers) | Map of autoscaling capacity providers created and their attributes | | [cloudwatch\_log\_group\_arn](#output\_cloudwatch\_log\_group\_arn) | ARN of CloudWatch log group created | | [cloudwatch\_log\_group\_name](#output\_cloudwatch\_log\_group\_name) | Name of CloudWatch log group created | | [cluster\_arn](#output\_cluster\_arn) | ARN that identifies the cluster | diff --git a/examples/complete/README.md b/examples/complete/README.md index 1d96ae73..2b87ca8b 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -14,9 +14,9 @@ Configuration in this directory creates: To run this example you need to execute: ```bash -$ terraform init -$ terraform plan -$ terraform apply +terraform init +terraform plan +terraform apply ``` Note that this example may create resources which will incur monetary charges on your AWS bill. Run `terraform destroy` when you no longer need these resources. @@ -66,12 +66,26 @@ No inputs. | Name | Description | |------|-------------| | [alb\_dns\_name](#output\_alb\_dns\_name) | The DNS name of the load balancer | +| [capacity\_providers](#output\_capacity\_providers) | Map of autoscaling capacity providers created and their attributes | +| [cloudwatch\_log\_group\_arn](#output\_cloudwatch\_log\_group\_arn) | ARN of CloudWatch log group created | +| [cloudwatch\_log\_group\_name](#output\_cloudwatch\_log\_group\_name) | Name of CloudWatch log group created | | [cluster\_arn](#output\_cluster\_arn) | ARN that identifies the cluster | -| [cluster\_autoscaling\_capacity\_providers](#output\_cluster\_autoscaling\_capacity\_providers) | Map of capacity providers created and their attributes | | [cluster\_capacity\_providers](#output\_cluster\_capacity\_providers) | Map of cluster capacity providers attributes | | [cluster\_id](#output\_cluster\_id) | ID that identifies the cluster | | [cluster\_name](#output\_cluster\_name) | Name that identifies the cluster | +| [infrastructure\_iam\_role\_arn](#output\_infrastructure\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [infrastructure\_iam\_role\_name](#output\_infrastructure\_iam\_role\_name) | IAM role name | +| [infrastructure\_iam\_role\_unique\_id](#output\_infrastructure\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | +| [node\_iam\_instance\_profile\_arn](#output\_node\_iam\_instance\_profile\_arn) | ARN assigned by AWS to the instance profile | +| [node\_iam\_instance\_profile\_id](#output\_node\_iam\_instance\_profile\_id) | Instance profile's ID | +| [node\_iam\_instance\_profile\_unique](#output\_node\_iam\_instance\_profile\_unique) | Stable and unique string identifying the IAM instance profile | +| [node\_iam\_role\_arn](#output\_node\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [node\_iam\_role\_name](#output\_node\_iam\_role\_name) | IAM role name | +| [node\_iam\_role\_unique\_id](#output\_node\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | | [services](#output\_services) | Map of services created and their attributes | +| [task\_exec\_iam\_role\_arn](#output\_task\_exec\_iam\_role\_arn) | Task execution IAM role ARN | +| [task\_exec\_iam\_role\_name](#output\_task\_exec\_iam\_role\_name) | Task execution IAM role name | +| [task\_exec\_iam\_role\_unique\_id](#output\_task\_exec\_iam\_role\_unique\_id) | Stable and unique string identifying the task execution IAM role | ## License diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index dd0cdd27..e261408b 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -17,14 +17,84 @@ output "cluster_name" { value = module.ecs.cluster_name } +output "cloudwatch_log_group_name" { + description = "Name of CloudWatch log group created" + value = module.ecs.cloudwatch_log_group_name +} + +output "cloudwatch_log_group_arn" { + description = "ARN of CloudWatch log group created" + value = module.ecs.cloudwatch_log_group_arn +} + output "cluster_capacity_providers" { description = "Map of cluster capacity providers attributes" value = module.ecs.cluster_capacity_providers } -output "cluster_autoscaling_capacity_providers" { - description = "Map of capacity providers created and their attributes" - value = module.ecs.autoscaling_capacity_providers +output "capacity_providers" { + description = "Map of autoscaling capacity providers created and their attributes" + value = module.ecs.capacity_providers +} + +output "task_exec_iam_role_name" { + description = "Task execution IAM role name" + value = module.ecs.task_exec_iam_role_name +} + +output "task_exec_iam_role_arn" { + description = "Task execution IAM role ARN" + value = module.ecs.task_exec_iam_role_arn +} + +output "task_exec_iam_role_unique_id" { + description = "Stable and unique string identifying the task execution IAM role" + value = module.ecs.task_exec_iam_role_unique_id +} + +output "infrastructure_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = module.ecs.infrastructure_iam_role_arn +} + +output "infrastructure_iam_role_name" { + description = "IAM role name" + value = module.ecs.infrastructure_iam_role_name +} + +output "infrastructure_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = module.ecs.infrastructure_iam_role_unique_id +} + +output "node_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = module.ecs.node_iam_role_arn +} + +output "node_iam_role_name" { + description = "IAM role name" + value = module.ecs.node_iam_role_name +} + +output "node_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = module.ecs.node_iam_role_unique_id +} + +output "node_iam_instance_profile_arn" { + description = "ARN assigned by AWS to the instance profile" + value = module.ecs.node_iam_instance_profile_arn +} + +output "node_iam_instance_profile_id" { + description = "Instance profile's ID" + value = module.ecs.node_iam_instance_profile_id +} + +output "node_iam_instance_profile_unique" { + description = "Stable and unique string identifying the IAM instance profile" + value = module.ecs.node_iam_instance_profile_unique } ################################################################################ diff --git a/examples/container-definition/README.md b/examples/container-definition/README.md index bfa6280d..15e50969 100644 --- a/examples/container-definition/README.md +++ b/examples/container-definition/README.md @@ -9,9 +9,9 @@ Configuration in this directory creates: To run this example you need to execute: ```bash -$ terraform init -$ terraform plan -$ terraform apply +terraform init +terraform plan +terraform apply ``` Note that this example may create resources which will incur monetary charges on your AWS bill. Run `terraform destroy` when you no longer need these resources. diff --git a/examples/ec2-autoscaling/README.md b/examples/ec2-autoscaling/README.md index a440d1b7..756e5367 100644 --- a/examples/ec2-autoscaling/README.md +++ b/examples/ec2-autoscaling/README.md @@ -14,9 +14,9 @@ Configuration in this directory creates: To run this example you need to execute: ```bash -$ terraform init -$ terraform plan -$ terraform apply +terraform init +terraform plan +terraform apply ``` Note that this example may create resources which will incur monetary charges on your AWS bill. Run `terraform destroy` when you no longer need these resources. @@ -62,11 +62,22 @@ No inputs. | Name | Description | |------|-------------| | [alb\_dns\_name](#output\_alb\_dns\_name) | The DNS name of the load balancer | +| [capacity\_providers](#output\_capacity\_providers) | Map of autoscaling capacity providers created and their attributes | +| [cloudwatch\_log\_group\_arn](#output\_cloudwatch\_log\_group\_arn) | ARN of CloudWatch log group created | +| [cloudwatch\_log\_group\_name](#output\_cloudwatch\_log\_group\_name) | Name of CloudWatch log group created | | [cluster\_arn](#output\_cluster\_arn) | ARN that identifies the cluster | -| [cluster\_autoscaling\_capacity\_providers](#output\_cluster\_autoscaling\_capacity\_providers) | Map of capacity providers created and their attributes | | [cluster\_capacity\_providers](#output\_cluster\_capacity\_providers) | Map of cluster capacity providers attributes | | [cluster\_id](#output\_cluster\_id) | ID that identifies the cluster | | [cluster\_name](#output\_cluster\_name) | Name that identifies the cluster | +| [infrastructure\_iam\_role\_arn](#output\_infrastructure\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [infrastructure\_iam\_role\_name](#output\_infrastructure\_iam\_role\_name) | IAM role name | +| [infrastructure\_iam\_role\_unique\_id](#output\_infrastructure\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | +| [node\_iam\_instance\_profile\_arn](#output\_node\_iam\_instance\_profile\_arn) | ARN assigned by AWS to the instance profile | +| [node\_iam\_instance\_profile\_id](#output\_node\_iam\_instance\_profile\_id) | Instance profile's ID | +| [node\_iam\_instance\_profile\_unique](#output\_node\_iam\_instance\_profile\_unique) | Stable and unique string identifying the IAM instance profile | +| [node\_iam\_role\_arn](#output\_node\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [node\_iam\_role\_name](#output\_node\_iam\_role\_name) | IAM role name | +| [node\_iam\_role\_unique\_id](#output\_node\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | | [service\_autoscaling\_policies](#output\_service\_autoscaling\_policies) | Map of autoscaling policies and their attributes | | [service\_autoscaling\_scheduled\_actions](#output\_service\_autoscaling\_scheduled\_actions) | Map of autoscaling scheduled actions and their attributes | | [service\_container\_definitions](#output\_service\_container\_definitions) | Container definitions | @@ -89,6 +100,9 @@ No inputs. | [service\_tasks\_iam\_role\_arn](#output\_service\_tasks\_iam\_role\_arn) | Tasks IAM role ARN | | [service\_tasks\_iam\_role\_name](#output\_service\_tasks\_iam\_role\_name) | Tasks IAM role name | | [service\_tasks\_iam\_role\_unique\_id](#output\_service\_tasks\_iam\_role\_unique\_id) | Stable and unique string identifying the tasks IAM role | +| [task\_exec\_iam\_role\_arn](#output\_task\_exec\_iam\_role\_arn) | Task execution IAM role ARN | +| [task\_exec\_iam\_role\_name](#output\_task\_exec\_iam\_role\_name) | Task execution IAM role name | +| [task\_exec\_iam\_role\_unique\_id](#output\_task\_exec\_iam\_role\_unique\_id) | Stable and unique string identifying the task execution IAM role | ## License diff --git a/examples/ec2-autoscaling/outputs.tf b/examples/ec2-autoscaling/outputs.tf index 3ad62947..9179fd9a 100644 --- a/examples/ec2-autoscaling/outputs.tf +++ b/examples/ec2-autoscaling/outputs.tf @@ -17,14 +17,84 @@ output "cluster_name" { value = module.ecs_cluster.name } +output "cloudwatch_log_group_name" { + description = "Name of CloudWatch log group created" + value = module.ecs_cluster.cloudwatch_log_group_name +} + +output "cloudwatch_log_group_arn" { + description = "ARN of CloudWatch log group created" + value = module.ecs_cluster.cloudwatch_log_group_arn +} + output "cluster_capacity_providers" { description = "Map of cluster capacity providers attributes" value = module.ecs_cluster.cluster_capacity_providers } -output "cluster_autoscaling_capacity_providers" { - description = "Map of capacity providers created and their attributes" - value = module.ecs_cluster.autoscaling_capacity_providers +output "capacity_providers" { + description = "Map of autoscaling capacity providers created and their attributes" + value = module.ecs_cluster.capacity_providers +} + +output "task_exec_iam_role_name" { + description = "Task execution IAM role name" + value = module.ecs_cluster.task_exec_iam_role_name +} + +output "task_exec_iam_role_arn" { + description = "Task execution IAM role ARN" + value = module.ecs_cluster.task_exec_iam_role_arn +} + +output "task_exec_iam_role_unique_id" { + description = "Stable and unique string identifying the task execution IAM role" + value = module.ecs_cluster.task_exec_iam_role_unique_id +} + +output "infrastructure_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = module.ecs_cluster.infrastructure_iam_role_arn +} + +output "infrastructure_iam_role_name" { + description = "IAM role name" + value = module.ecs_cluster.infrastructure_iam_role_name +} + +output "infrastructure_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = module.ecs_cluster.infrastructure_iam_role_unique_id +} + +output "node_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = module.ecs_cluster.node_iam_role_arn +} + +output "node_iam_role_name" { + description = "IAM role name" + value = module.ecs_cluster.node_iam_role_name +} + +output "node_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = module.ecs_cluster.node_iam_role_unique_id +} + +output "node_iam_instance_profile_arn" { + description = "ARN assigned by AWS to the instance profile" + value = module.ecs_cluster.node_iam_instance_profile_arn +} + +output "node_iam_instance_profile_id" { + description = "Instance profile's ID" + value = module.ecs_cluster.node_iam_instance_profile_id +} + +output "node_iam_instance_profile_unique" { + description = "Stable and unique string identifying the IAM instance profile" + value = module.ecs_cluster.node_iam_instance_profile_unique } ################################################################################ diff --git a/examples/fargate/README.md b/examples/fargate/README.md index e0cb98b0..65ae93a7 100644 --- a/examples/fargate/README.md +++ b/examples/fargate/README.md @@ -14,9 +14,9 @@ Configuration in this directory creates: To run this example you need to execute: ```bash -$ terraform init -$ terraform plan -$ terraform apply +terraform init +terraform plan +terraform apply ``` Note that this example may create resources which will incur monetary charges on your AWS bill. Run `terraform destroy` when you no longer need these resources. @@ -65,11 +65,22 @@ No inputs. | Name | Description | |------|-------------| | [alb\_dns\_name](#output\_alb\_dns\_name) | The DNS name of the load balancer | +| [capacity\_providers](#output\_capacity\_providers) | Map of autoscaling capacity providers created and their attributes | +| [cloudwatch\_log\_group\_arn](#output\_cloudwatch\_log\_group\_arn) | ARN of CloudWatch log group created | +| [cloudwatch\_log\_group\_name](#output\_cloudwatch\_log\_group\_name) | Name of CloudWatch log group created | | [cluster\_arn](#output\_cluster\_arn) | ARN that identifies the cluster | -| [cluster\_autoscaling\_capacity\_providers](#output\_cluster\_autoscaling\_capacity\_providers) | Map of capacity providers created and their attributes | | [cluster\_capacity\_providers](#output\_cluster\_capacity\_providers) | Map of cluster capacity providers attributes | | [cluster\_id](#output\_cluster\_id) | ID that identifies the cluster | | [cluster\_name](#output\_cluster\_name) | Name that identifies the cluster | +| [infrastructure\_iam\_role\_arn](#output\_infrastructure\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [infrastructure\_iam\_role\_name](#output\_infrastructure\_iam\_role\_name) | IAM role name | +| [infrastructure\_iam\_role\_unique\_id](#output\_infrastructure\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | +| [node\_iam\_instance\_profile\_arn](#output\_node\_iam\_instance\_profile\_arn) | ARN assigned by AWS to the instance profile | +| [node\_iam\_instance\_profile\_id](#output\_node\_iam\_instance\_profile\_id) | Instance profile's ID | +| [node\_iam\_instance\_profile\_unique](#output\_node\_iam\_instance\_profile\_unique) | Stable and unique string identifying the IAM instance profile | +| [node\_iam\_role\_arn](#output\_node\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [node\_iam\_role\_name](#output\_node\_iam\_role\_name) | IAM role name | +| [node\_iam\_role\_unique\_id](#output\_node\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | | [service\_autoscaling\_policies](#output\_service\_autoscaling\_policies) | Map of autoscaling policies and their attributes | | [service\_autoscaling\_scheduled\_actions](#output\_service\_autoscaling\_scheduled\_actions) | Map of autoscaling scheduled actions and their attributes | | [service\_container\_definitions](#output\_service\_container\_definitions) | Container definitions | @@ -94,6 +105,9 @@ No inputs. | [service\_tasks\_iam\_role\_name](#output\_service\_tasks\_iam\_role\_name) | Tasks IAM role name | | [service\_tasks\_iam\_role\_unique\_id](#output\_service\_tasks\_iam\_role\_unique\_id) | Stable and unique string identifying the tasks IAM role | | [task\_definition\_run\_task\_command](#output\_task\_definition\_run\_task\_command) | awscli command to run the standalone task | +| [task\_exec\_iam\_role\_arn](#output\_task\_exec\_iam\_role\_arn) | Task execution IAM role ARN | +| [task\_exec\_iam\_role\_name](#output\_task\_exec\_iam\_role\_name) | Task execution IAM role name | +| [task\_exec\_iam\_role\_unique\_id](#output\_task\_exec\_iam\_role\_unique\_id) | Stable and unique string identifying the task execution IAM role | ## License diff --git a/examples/fargate/outputs.tf b/examples/fargate/outputs.tf index e31035a5..3e8a7657 100644 --- a/examples/fargate/outputs.tf +++ b/examples/fargate/outputs.tf @@ -17,14 +17,84 @@ output "cluster_name" { value = module.ecs_cluster.name } +output "cloudwatch_log_group_name" { + description = "Name of CloudWatch log group created" + value = module.ecs_cluster.cloudwatch_log_group_name +} + +output "cloudwatch_log_group_arn" { + description = "ARN of CloudWatch log group created" + value = module.ecs_cluster.cloudwatch_log_group_arn +} + output "cluster_capacity_providers" { description = "Map of cluster capacity providers attributes" value = module.ecs_cluster.cluster_capacity_providers } -output "cluster_autoscaling_capacity_providers" { - description = "Map of capacity providers created and their attributes" - value = module.ecs_cluster.autoscaling_capacity_providers +output "capacity_providers" { + description = "Map of autoscaling capacity providers created and their attributes" + value = module.ecs_cluster.capacity_providers +} + +output "task_exec_iam_role_name" { + description = "Task execution IAM role name" + value = module.ecs_cluster.task_exec_iam_role_name +} + +output "task_exec_iam_role_arn" { + description = "Task execution IAM role ARN" + value = module.ecs_cluster.task_exec_iam_role_arn +} + +output "task_exec_iam_role_unique_id" { + description = "Stable and unique string identifying the task execution IAM role" + value = module.ecs_cluster.task_exec_iam_role_unique_id +} + +output "infrastructure_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = module.ecs_cluster.infrastructure_iam_role_arn +} + +output "infrastructure_iam_role_name" { + description = "IAM role name" + value = module.ecs_cluster.infrastructure_iam_role_name +} + +output "infrastructure_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = module.ecs_cluster.infrastructure_iam_role_unique_id +} + +output "node_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = module.ecs_cluster.node_iam_role_arn +} + +output "node_iam_role_name" { + description = "IAM role name" + value = module.ecs_cluster.node_iam_role_name +} + +output "node_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = module.ecs_cluster.node_iam_role_unique_id +} + +output "node_iam_instance_profile_arn" { + description = "ARN assigned by AWS to the instance profile" + value = module.ecs_cluster.node_iam_instance_profile_arn +} + +output "node_iam_instance_profile_id" { + description = "Instance profile's ID" + value = module.ecs_cluster.node_iam_instance_profile_id +} + +output "node_iam_instance_profile_unique" { + description = "Stable and unique string identifying the IAM instance profile" + value = module.ecs_cluster.node_iam_instance_profile_unique } ################################################################################ diff --git a/examples/managed-instances/README.md b/examples/managed-instances/README.md new file mode 100644 index 00000000..dfaed040 --- /dev/null +++ b/examples/managed-instances/README.md @@ -0,0 +1,106 @@ +# ECS Clusters w/ ECS Managed Instances + +Configuration in this directory creates: + +- ECS cluster using ECS Managed Instances capacity provider +- Example ECS service that utilizes + - AWS Firelens using FluentBit sidecar container definition + - Service connect configuration + - Load balancer target group attachment + - Security group for access to the example service + +## Usage + +To run this example you need to execute: + +```bash +terraform init +terraform plan +terraform apply +``` + +Note that this example may create resources which will incur monetary charges on your AWS bill. Run `terraform destroy` when you no longer need these resources. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.5.7 | +| [aws](#requirement\_aws) | >= 6.15 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 6.15 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [ecs\_cluster](#module\_ecs\_cluster) | ../../modules/cluster | n/a | +| [ecs\_service](#module\_ecs\_service) | ../../modules/service | n/a | +| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 6.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | + +## Inputs + +No inputs. + +## Outputs + +| Name | Description | +|------|-------------| +| [capacity\_providers](#output\_capacity\_providers) | Map of autoscaling capacity providers created and their attributes | +| [cloudwatch\_log\_group\_arn](#output\_cloudwatch\_log\_group\_arn) | ARN of CloudWatch log group created | +| [cloudwatch\_log\_group\_name](#output\_cloudwatch\_log\_group\_name) | Name of CloudWatch log group created | +| [cluster\_arn](#output\_cluster\_arn) | ARN that identifies the cluster | +| [cluster\_capacity\_providers](#output\_cluster\_capacity\_providers) | Map of cluster capacity providers attributes | +| [cluster\_id](#output\_cluster\_id) | ID that identifies the cluster | +| [cluster\_name](#output\_cluster\_name) | Name that identifies the cluster | +| [infrastructure\_iam\_role\_arn](#output\_infrastructure\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [infrastructure\_iam\_role\_name](#output\_infrastructure\_iam\_role\_name) | IAM role name | +| [infrastructure\_iam\_role\_unique\_id](#output\_infrastructure\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | +| [node\_iam\_instance\_profile\_arn](#output\_node\_iam\_instance\_profile\_arn) | ARN assigned by AWS to the instance profile | +| [node\_iam\_instance\_profile\_id](#output\_node\_iam\_instance\_profile\_id) | Instance profile's ID | +| [node\_iam\_instance\_profile\_unique](#output\_node\_iam\_instance\_profile\_unique) | Stable and unique string identifying the IAM instance profile | +| [node\_iam\_role\_arn](#output\_node\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [node\_iam\_role\_name](#output\_node\_iam\_role\_name) | IAM role name | +| [node\_iam\_role\_unique\_id](#output\_node\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | +| [service\_autoscaling\_policies](#output\_service\_autoscaling\_policies) | Map of autoscaling policies and their attributes | +| [service\_autoscaling\_scheduled\_actions](#output\_service\_autoscaling\_scheduled\_actions) | Map of autoscaling scheduled actions and their attributes | +| [service\_container\_definitions](#output\_service\_container\_definitions) | Container definitions | +| [service\_iam\_role\_arn](#output\_service\_iam\_role\_arn) | Service IAM role ARN | +| [service\_iam\_role\_name](#output\_service\_iam\_role\_name) | Service IAM role name | +| [service\_iam\_role\_unique\_id](#output\_service\_iam\_role\_unique\_id) | Stable and unique string identifying the service IAM role | +| [service\_id](#output\_service\_id) | ARN that identifies the service | +| [service\_name](#output\_service\_name) | Name of the service | +| [service\_security\_group\_arn](#output\_service\_security\_group\_arn) | Amazon Resource Name (ARN) of the security group | +| [service\_security\_group\_id](#output\_service\_security\_group\_id) | ID of the security group | +| [service\_task\_definition\_arn](#output\_service\_task\_definition\_arn) | Full ARN of the Task Definition (including both `family` and `revision`) | +| [service\_task\_definition\_family](#output\_service\_task\_definition\_family) | The unique name of the task definition | +| [service\_task\_definition\_revision](#output\_service\_task\_definition\_revision) | Revision of the task in a particular family | +| [service\_task\_exec\_iam\_role\_arn](#output\_service\_task\_exec\_iam\_role\_arn) | Task execution IAM role ARN | +| [service\_task\_exec\_iam\_role\_name](#output\_service\_task\_exec\_iam\_role\_name) | Task execution IAM role name | +| [service\_task\_exec\_iam\_role\_unique\_id](#output\_service\_task\_exec\_iam\_role\_unique\_id) | Stable and unique string identifying the task execution IAM role | +| [service\_task\_set\_arn](#output\_service\_task\_set\_arn) | The Amazon Resource Name (ARN) that identifies the task set | +| [service\_task\_set\_id](#output\_service\_task\_set\_id) | The ID of the task set | +| [service\_task\_set\_stability\_status](#output\_service\_task\_set\_stability\_status) | The stability status. This indicates whether the task set has reached a steady state | +| [service\_task\_set\_status](#output\_service\_task\_set\_status) | The status of the task set | +| [service\_tasks\_iam\_role\_arn](#output\_service\_tasks\_iam\_role\_arn) | Tasks IAM role ARN | +| [service\_tasks\_iam\_role\_name](#output\_service\_tasks\_iam\_role\_name) | Tasks IAM role name | +| [service\_tasks\_iam\_role\_unique\_id](#output\_service\_tasks\_iam\_role\_unique\_id) | Stable and unique string identifying the tasks IAM role | +| [task\_exec\_iam\_role\_arn](#output\_task\_exec\_iam\_role\_arn) | Task execution IAM role ARN | +| [task\_exec\_iam\_role\_name](#output\_task\_exec\_iam\_role\_name) | Task execution IAM role name | +| [task\_exec\_iam\_role\_unique\_id](#output\_task\_exec\_iam\_role\_unique\_id) | Stable and unique string identifying the task execution IAM role | + + +## License + +Apache-2.0 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-ecs/blob/master/LICENSE). diff --git a/examples/managed-instances/main.tf b/examples/managed-instances/main.tf new file mode 100644 index 00000000..a838257f --- /dev/null +++ b/examples/managed-instances/main.tf @@ -0,0 +1,232 @@ +provider "aws" { + region = local.region +} + +data "aws_availability_zones" "available" {} + +locals { + region = "eu-west-1" + name = "ex-${basename(path.cwd)}" + + vpc_cidr = "10.0.0.0/16" + azs = slice(data.aws_availability_zones.available.names, 0, 3) + + container_name = "ecs-sample" + container_port = 80 + + tags = { + Name = local.name + Example = local.name + Repository = "https://github.com/terraform-aws-modules/terraform-aws-ecs" + } +} + +################################################################################ +# Cluster +################################################################################ + +module "ecs_cluster" { + source = "../../modules/cluster" + + name = local.name + + capacity_providers = { + mi-example = { + managed_instances_provider = { + instance_launch_template = { + instance_requirements = { + instance_generations = ["current"] + cpu_manufacturers = ["intel", "amd"] + + memory_mib = { + max = 8192 + min = 1024 + } + + vcpu_count = { + max = 4 + min = 1 + } + } + + network_configuration = { + subnets = module.vpc.private_subnets + # security_groups = [aws_security_group.example.id] + } + + storage_configuration = { + storage_size_gib = 30 + } + } + } + } + } + + # Managed instances security group + vpc_id = module.vpc.vpc_id + security_group_ingress_rules = { + vpc = { + cidr_ipv4 = module.vpc.vpc_cidr_block + from_port = local.container_port + to_port = local.container_port + } + } + + tags = local.tags +} + +################################################################################ +# Service +################################################################################ + +module "ecs_service" { + source = "../../modules/service" + + # Service + name = local.name + cluster_arn = module.ecs_cluster.arn + + # Task Definition + requires_compatibilities = ["MANAGED_INSTANCES"] + launch_type = "EC2" + + # Container definition(s) + container_definitions = { + (local.container_name) = { + essential = true + image = "public.ecr.aws/docker/library/httpd:latest" + + cpu = 1024 + memory = 2048 + + portMappings = [ + { + name = local.container_name + containerPort = local.container_port + hostPort = local.container_port + protocol = "tcp" + } + ] + } + } + + # TODO - this shouldn't be required + capacity_provider_strategy = { + # On-demand instances + mi-example = { + capacity_provider = module.ecs_cluster.capacity_providers["mi-example"].name + weight = 1 + base = 1 + } + } + + # load_balancer = { + # service = { + # target_group_arn = module.alb.target_groups["ex_ecs"].arn + # container_name = local.container_name + # container_port = local.container_port + # } + # } + + subnet_ids = module.vpc.private_subnets + security_group_ingress_rules = { + alb_http = { + from_port = local.container_port + description = "Service port" + cidr_ipv4 = module.vpc.vpc_cidr_block + } + } + + tags = local.tags +} + +################################################################################ +# Supporting Resources +################################################################################ + +# module "alb" { +# source = "terraform-aws-modules/alb/aws" +# version = "~> 10.0" + +# name = local.name + +# load_balancer_type = "application" + +# vpc_id = module.vpc.vpc_id +# subnets = module.vpc.public_subnets + +# # For example only +# enable_deletion_protection = false + +# # Security Group +# security_group_ingress_rules = { +# all_http = { +# from_port = 80 +# to_port = 80 +# ip_protocol = "tcp" +# cidr_ipv4 = "0.0.0.0/0" +# } +# } +# security_group_egress_rules = { +# all = { +# ip_protocol = "-1" +# cidr_ipv4 = module.vpc.vpc_cidr_block +# } +# } + +# listeners = { +# ex_http = { +# port = 80 +# protocol = "HTTP" + +# forward = { +# target_group_key = "ex_ecs" +# } +# } +# } + +# target_groups = { +# ex_ecs = { +# backend_protocol = "HTTP" +# backend_port = local.container_port +# target_type = "ip" +# deregistration_delay = 5 +# load_balancing_cross_zone_enabled = true + +# health_check = { +# enabled = true +# healthy_threshold = 5 +# interval = 30 +# matcher = "200" +# path = "/" +# port = "traffic-port" +# protocol = "HTTP" +# timeout = 5 +# unhealthy_threshold = 2 +# } + +# # Theres nothing to attach here in this definition. Instead, +# # ECS will attach the IPs of the tasks to this target group +# create_attachment = false +# } +# } + +# tags = local.tags +# } + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 6.0" + + name = local.name + cidr = local.vpc_cidr + + azs = local.azs + private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)] + public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 48)] + + enable_nat_gateway = true + single_nat_gateway = true + + tags = local.tags +} diff --git a/examples/managed-instances/outputs.tf b/examples/managed-instances/outputs.tf new file mode 100644 index 00000000..43506223 --- /dev/null +++ b/examples/managed-instances/outputs.tf @@ -0,0 +1,217 @@ +################################################################################ +# Cluster +################################################################################ + +output "cluster_arn" { + description = "ARN that identifies the cluster" + value = module.ecs_cluster.arn +} + +output "cluster_id" { + description = "ID that identifies the cluster" + value = module.ecs_cluster.id +} + +output "cluster_name" { + description = "Name that identifies the cluster" + value = module.ecs_cluster.name +} + +output "cloudwatch_log_group_name" { + description = "Name of CloudWatch log group created" + value = module.ecs_cluster.cloudwatch_log_group_name +} + +output "cloudwatch_log_group_arn" { + description = "ARN of CloudWatch log group created" + value = module.ecs_cluster.cloudwatch_log_group_arn +} + +output "cluster_capacity_providers" { + description = "Map of cluster capacity providers attributes" + value = module.ecs_cluster.cluster_capacity_providers +} + +output "capacity_providers" { + description = "Map of autoscaling capacity providers created and their attributes" + value = module.ecs_cluster.capacity_providers +} + +output "task_exec_iam_role_name" { + description = "Task execution IAM role name" + value = module.ecs_cluster.task_exec_iam_role_name +} + +output "task_exec_iam_role_arn" { + description = "Task execution IAM role ARN" + value = module.ecs_cluster.task_exec_iam_role_arn +} + +output "task_exec_iam_role_unique_id" { + description = "Stable and unique string identifying the task execution IAM role" + value = module.ecs_cluster.task_exec_iam_role_unique_id +} + +output "infrastructure_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = module.ecs_cluster.infrastructure_iam_role_arn +} + +output "infrastructure_iam_role_name" { + description = "IAM role name" + value = module.ecs_cluster.infrastructure_iam_role_name +} + +output "infrastructure_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = module.ecs_cluster.infrastructure_iam_role_unique_id +} + +output "node_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = module.ecs_cluster.node_iam_role_arn +} + +output "node_iam_role_name" { + description = "IAM role name" + value = module.ecs_cluster.node_iam_role_name +} + +output "node_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = module.ecs_cluster.node_iam_role_unique_id +} + +output "node_iam_instance_profile_arn" { + description = "ARN assigned by AWS to the instance profile" + value = module.ecs_cluster.node_iam_instance_profile_arn +} + +output "node_iam_instance_profile_id" { + description = "Instance profile's ID" + value = module.ecs_cluster.node_iam_instance_profile_id +} + +output "node_iam_instance_profile_unique" { + description = "Stable and unique string identifying the IAM instance profile" + value = module.ecs_cluster.node_iam_instance_profile_unique +} + +################################################################################ +# Service +################################################################################ + +output "service_id" { + description = "ARN that identifies the service" + value = module.ecs_service.id +} + +output "service_name" { + description = "Name of the service" + value = module.ecs_service.name +} + +output "service_iam_role_name" { + description = "Service IAM role name" + value = module.ecs_service.iam_role_name +} + +output "service_iam_role_arn" { + description = "Service IAM role ARN" + value = module.ecs_service.iam_role_arn +} + +output "service_iam_role_unique_id" { + description = "Stable and unique string identifying the service IAM role" + value = module.ecs_service.iam_role_unique_id +} + +output "service_container_definitions" { + description = "Container definitions" + value = module.ecs_service.container_definitions +} + +output "service_task_definition_arn" { + description = "Full ARN of the Task Definition (including both `family` and `revision`)" + value = module.ecs_service.task_definition_arn +} + +output "service_task_definition_revision" { + description = "Revision of the task in a particular family" + value = module.ecs_service.task_definition_revision +} + +output "service_task_definition_family" { + description = "The unique name of the task definition" + value = module.ecs_service.task_definition_family +} + +output "service_task_exec_iam_role_name" { + description = "Task execution IAM role name" + value = module.ecs_service.task_exec_iam_role_name +} + +output "service_task_exec_iam_role_arn" { + description = "Task execution IAM role ARN" + value = module.ecs_service.task_exec_iam_role_arn +} + +output "service_task_exec_iam_role_unique_id" { + description = "Stable and unique string identifying the task execution IAM role" + value = module.ecs_service.task_exec_iam_role_unique_id +} + +output "service_tasks_iam_role_name" { + description = "Tasks IAM role name" + value = module.ecs_service.tasks_iam_role_name +} + +output "service_tasks_iam_role_arn" { + description = "Tasks IAM role ARN" + value = module.ecs_service.tasks_iam_role_arn +} + +output "service_tasks_iam_role_unique_id" { + description = "Stable and unique string identifying the tasks IAM role" + value = module.ecs_service.tasks_iam_role_unique_id +} + +output "service_task_set_id" { + description = "The ID of the task set" + value = module.ecs_service.task_set_id +} + +output "service_task_set_arn" { + description = "The Amazon Resource Name (ARN) that identifies the task set" + value = module.ecs_service.task_set_arn +} + +output "service_task_set_stability_status" { + description = "The stability status. This indicates whether the task set has reached a steady state" + value = module.ecs_service.task_set_stability_status +} + +output "service_task_set_status" { + description = "The status of the task set" + value = module.ecs_service.task_set_status +} + +output "service_autoscaling_policies" { + description = "Map of autoscaling policies and their attributes" + value = module.ecs_service.autoscaling_policies +} + +output "service_autoscaling_scheduled_actions" { + description = "Map of autoscaling scheduled actions and their attributes" + value = module.ecs_service.autoscaling_scheduled_actions +} + +output "service_security_group_arn" { + description = "Amazon Resource Name (ARN) of the security group" + value = module.ecs_service.security_group_arn +} + +output "service_security_group_id" { + description = "ID of the security group" + value = module.ecs_service.security_group_id +} diff --git a/examples/managed-instances/variables.tf b/examples/managed-instances/variables.tf new file mode 100644 index 00000000..e69de29b diff --git a/examples/managed-instances/versions.tf b/examples/managed-instances/versions.tf new file mode 100644 index 00000000..e1ac31ca --- /dev/null +++ b/examples/managed-instances/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.5.7" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.15" + } + } +} diff --git a/modules/cluster/README.md b/modules/cluster/README.md index e9070a3b..576fc6ef 100644 --- a/modules/cluster/README.md +++ b/modules/cluster/README.md @@ -166,6 +166,9 @@ No modules. | [aws_iam_role_policy_attachment.node_additional](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.task_exec_additional](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_security_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_vpc_security_group_egress_rule.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | +| [aws_vpc_security_group_ingress_rule.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy_document.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.node](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -180,7 +183,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [autoscaling\_capacity\_providers](#input\_autoscaling\_capacity\_providers) | [DEPRECATED - use `capacity_providers` instead] Map of autoscaling capacity provider definitions to create for the cluster |
map(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | -| [capacity\_providers](#input\_capacity\_providers) | Map of capacity provider definitions to create for the cluster |
map(object({
autoscaling_group_provider = optional(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
}))
managed_instances_provider = optional(object({
infrastructure_role_arn = optional(string)
instance_launch_template = object({
ec2_instance_profile_arn = optional(string)
instance_requirements = optional(object({
accelerator_count = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_manufacturers = optional(list(string))
accelerator_names = optional(list(string))
accelerator_total_memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_types = optional(list(string))
allowed_instance_types = optional(list(string))
bare_metal = optional(string)
baseline_ebs_bandwidth_mbps = optional(object({
max = optional(number)
min = optional(number)
}))
burstable_performance = optional(string)
cpu_manufacturers = optional(list(string))
excluded_instance_types = optional(list(string))
instance_generations = optional(list(string))
local_storage = optional(string)
local_storage_types = optional(list(string))
max_spot_price_as_percentage_of_optimal_on_demand_price = optional(number)
memory_gib_per_vcpu = optional(object({
max = optional(number)
min = optional(number)
}))
memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
network_bandwidth_gbps = optional(object({
max = optional(number)
min = optional(number)
}))
network_interface_count = optional(object({
max = optional(number)
min = optional(number)
}))
on_demand_max_price_percentage_over_lowest_price = optional(number)
require_hibernate_support = optional(bool)
spot_max_price_percentage_over_lowest_price = optional(number)
total_local_storage_gb = optional(object({
max = optional(number)
min = optional(number)
}))
vcpu_count = optional(object({
max = optional(number)
min = optional(number)
}))
}))
monitoring = optional(string)
network_configuration = optional(object({
security_groups = optional(list(string))
subnets = list(string)
}))
storage_configuration = optional(object({
storage_size_gib = number
}))
})
propagate_tags = optional(string)
}))
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | +| [capacity\_providers](#input\_capacity\_providers) | Map of capacity provider definitions to create for the cluster |
map(object({
auto_scaling_group_provider = optional(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
}))
managed_instances_provider = optional(object({
infrastructure_role_arn = optional(string)
instance_launch_template = object({
ec2_instance_profile_arn = optional(string)
instance_requirements = optional(object({
accelerator_count = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_manufacturers = optional(list(string))
accelerator_names = optional(list(string))
accelerator_total_memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_types = optional(list(string))
allowed_instance_types = optional(list(string))
bare_metal = optional(string)
baseline_ebs_bandwidth_mbps = optional(object({
max = optional(number)
min = optional(number)
}))
burstable_performance = optional(string)
cpu_manufacturers = optional(list(string))
excluded_instance_types = optional(list(string))
instance_generations = optional(list(string))
local_storage = optional(string)
local_storage_types = optional(list(string))
max_spot_price_as_percentage_of_optimal_on_demand_price = optional(number)
memory_gib_per_vcpu = optional(object({
max = optional(number)
min = optional(number)
}))
memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
network_bandwidth_gbps = optional(object({
max = optional(number)
min = optional(number)
}))
network_interface_count = optional(object({
max = optional(number)
min = optional(number)
}))
on_demand_max_price_percentage_over_lowest_price = optional(number)
require_hibernate_support = optional(bool)
spot_max_price_percentage_over_lowest_price = optional(number)
total_local_storage_gb = optional(object({
max = optional(number)
min = optional(number)
}))
vcpu_count = optional(object({
max = optional(number)
min = optional(number)
}))
}))
monitoring = optional(string)
network_configuration = optional(object({
security_groups = optional(list(string), [])
subnets = list(string)
}))
storage_configuration = optional(object({
storage_size_gib = number
}))
})
propagate_tags = optional(string, "CAPACITY_PROVIDER")
}))
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | | [cloudwatch\_log\_group\_class](#input\_cloudwatch\_log\_group\_class) | Specified the log class of the log group. Possible values are: `STANDARD` or `INFREQUENT_ACCESS` | `string` | `null` | no | | [cloudwatch\_log\_group\_kms\_key\_id](#input\_cloudwatch\_log\_group\_kms\_key\_id) | If a KMS Key ARN is set, this key will be used to encrypt the corresponding log group. Please be sure that the KMS Key has an appropriate key policy (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html) | `string` | `null` | no | | [cloudwatch\_log\_group\_name](#input\_cloudwatch\_log\_group\_name) | Custom name of CloudWatch Log Group for ECS cluster | `string` | `null` | no | @@ -191,6 +194,7 @@ No modules. | [create\_cloudwatch\_log\_group](#input\_create\_cloudwatch\_log\_group) | Determines whether a log group is created by this module for the cluster logs. If not, AWS will automatically create one if logging is enabled | `bool` | `true` | no | | [create\_infrastructure\_iam\_role](#input\_create\_infrastructure\_iam\_role) | Determines whether the ECS infrastructure IAM role should be created | `bool` | `true` | no | | [create\_node\_iam\_instance\_profile](#input\_create\_node\_iam\_instance\_profile) | Determines whether an IAM instance profile is created or to use an existing IAM instance profile | `bool` | `true` | no | +| [create\_security\_group](#input\_create\_security\_group) | Determines if a security group is created | `bool` | `true` | no | | [create\_task\_exec\_iam\_role](#input\_create\_task\_exec\_iam\_role) | Determines whether the ECS task definition IAM role should be created | `bool` | `false` | no | | [create\_task\_exec\_policy](#input\_create\_task\_exec\_policy) | Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters | `bool` | `true` | no | | [default\_capacity\_provider\_strategy](#input\_default\_capacity\_provider\_strategy) | Map of default capacity provider strategy definitions to use for the cluster |
map(object({
base = optional(number)
name = optional(string) # Will fall back to use map key if not set
weight = optional(number)
}))
| `{}` | no | @@ -212,6 +216,12 @@ No modules. | [node\_iam\_role\_tags](#input\_node\_iam\_role\_tags) | A map of additional tags to add to the IAM role/instance profile created | `map(string)` | `{}` | no | | [node\_iam\_role\_use\_name\_prefix](#input\_node\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role/instance profile name (`node_iam_role_name`) is used as a prefix | `bool` | `true` | no | | [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | +| [security\_group\_description](#input\_security\_group\_description) | Description of the security group created | `string` | `null` | no | +| [security\_group\_egress\_rules](#input\_security\_group\_egress\_rules) | Security group egress rules to add to the security group created |
map(object({
name = optional(string)

cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
}))
| `{}` | no | +| [security\_group\_ingress\_rules](#input\_security\_group\_ingress\_rules) | Security group ingress rules to add to the security group created |
map(object({
name = optional(string)

cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
}))
| `{}` | no | +| [security\_group\_name](#input\_security\_group\_name) | Name to use on security group created | `string` | `null` | no | +| [security\_group\_tags](#input\_security\_group\_tags) | A map of additional tags to add to the security group created | `map(string)` | `{}` | no | +| [security\_group\_use\_name\_prefix](#input\_security\_group\_use\_name\_prefix) | Determines whether the security group name (`security_group_name`) is used as a prefix | `bool` | `true` | no | | [service\_connect\_defaults](#input\_service\_connect\_defaults) | Configures a default Service Connect namespace |
object({
namespace = string
})
| `null` | no | | [setting](#input\_setting) | List of configuration block(s) with cluster settings. For example, this can be used to enable CloudWatch Container Insights for a cluster |
list(object({
name = string
value = string
}))
|
[
{
"name": "containerInsights",
"value": "enabled"
}
]
| no | | [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | @@ -225,13 +235,15 @@ No modules. | [task\_exec\_iam\_statements](#input\_task\_exec\_iam\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage |
map(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string, "Allow")
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
variable = string
values = list(string)
})))
}))
| `null` | no | | [task\_exec\_secret\_arns](#input\_task\_exec\_secret\_arns) | List of SecretsManager secret ARNs the task execution role will be permitted to get/read | `list(string)` | `[]` | no | | [task\_exec\_ssm\_param\_arns](#input\_task\_exec\_ssm\_param\_arns) | List of SSM parameter ARNs the task execution role will be permitted to get/read | `list(string)` | `[]` | no | +| [vpc\_id](#input\_vpc\_id) | The ID of the VPC where the security group will be created | `string` | `null` | no | ## Outputs | Name | Description | |------|-------------| | [arn](#output\_arn) | ARN that identifies the cluster | -| [autoscaling\_capacity\_providers](#output\_autoscaling\_capacity\_providers) | Map of autoscaling capacity providers created and their attributes | +| [autoscaling\_capacity\_providers](#output\_autoscaling\_capacity\_providers) | [DEPRECATED - use `capacity_providers`] Map of autoscaling capacity providers created and their attributes | +| [capacity\_providers](#output\_capacity\_providers) | Map of autoscaling capacity providers created and their attributes | | [cloudwatch\_log\_group\_arn](#output\_cloudwatch\_log\_group\_arn) | ARN of CloudWatch log group created | | [cloudwatch\_log\_group\_name](#output\_cloudwatch\_log\_group\_name) | Name of CloudWatch log group created | | [cluster\_capacity\_providers](#output\_cluster\_capacity\_providers) | Map of cluster capacity providers attributes | diff --git a/modules/cluster/main.tf b/modules/cluster/main.tf index c442e085..0971fe85 100644 --- a/modules/cluster/main.tf +++ b/modules/cluster/main.tf @@ -113,6 +113,12 @@ resource "aws_cloudwatch_log_group" "this" { # Cluster Capacity Providers ################################################################################ +locals { + # TODO - is this correct?! + # Only the AutoScaling group capacity providers need to be associated with the cluster + auto_scaling_group_providers = local.capacity_providers != null ? [for k, v in local.capacity_providers : try(coalesce(v.name, k)) if v.auto_scaling_group_provider != null] : [] +} + resource "aws_ecs_cluster_capacity_providers" "this" { count = var.create ? 1 : 0 @@ -121,7 +127,7 @@ resource "aws_ecs_cluster_capacity_providers" "this" { cluster_name = aws_ecs_cluster.this[0].name capacity_providers = distinct(concat( [for k, v in var.default_capacity_provider_strategy : try(coalesce(v.name, k))], - var.autoscaling_capacity_providers != null ? [for k, v in var.autoscaling_capacity_providers : try(coalesce(v.name, k))] : [] + local.auto_scaling_group_providers )) # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cluster-capacity-providers.html#capacity-providers-considerations @@ -145,14 +151,14 @@ resource "aws_ecs_cluster_capacity_providers" "this" { ################################################################################ locals { - managed_instances_enabled = anytrue([for k, v in local.capacity_providers : v.managed_instances_provider != null]) + managed_instances_enabled = anytrue([for k, v in local.capacity_providers : v.managed_instances_provider != null]) # TODO - embed the `autoscaling_capacity_providers` into a shape acceptable for # `var.capacity_providers` so that it can be merged with the new `capacity_providers` # for backward compatibility. Remove `autoscaling_capacity_providers` in the next major version. capacity_providers = var.autoscaling_capacity_providers != null ? merge({ for k, v in var.autoscaling_capacity_providers : k => { - autoscaling_group_provider = { + auto_scaling_group_provider = { autoscaling_group_arn = v.autoscaling_group_arn managed_draining = try(v.managed_draining, null) managed_scaling = try(v.managed_scaling, null) != null ? { @@ -343,7 +349,8 @@ resource "aws_ecs_capacity_provider" "this" { } } - cluster = each.value.managed_instances_provider != null ? aws_ecs_cluster.this[0].id : null + # cluster = each.value.managed_instances_provider != null ? aws_ecs_cluster.this[0].id : null + cluster = aws_ecs_cluster.this[0].name name = try(coalesce(each.value.name, each.key), "") @@ -514,7 +521,7 @@ resource "aws_iam_role_policy_attachment" "task_exec" { locals { create_infrastructure_iam_role = var.create && var.create_infrastructure_iam_role && local.managed_instances_enabled - infrastructure_iam_role_name = coalesce(var.infrastructure_iam_role_name, "${var.name}-infra", "NotProvided") + infrastructure_iam_role_name = coalesce(var.infrastructure_iam_role_name, "${var.name}-infra", "NotProvided") } data "aws_iam_policy_document" "infrastructure" { @@ -749,7 +756,7 @@ resource "aws_iam_instance_profile" "this" { locals { create_security_group = var.create && var.create_security_group && local.managed_instances_enabled - security_group_name = coalesce(var.security_group_name, var.name, "NotProvided") + security_group_name = coalesce(var.security_group_name, var.name, "NotProvided") } resource "aws_security_group" "this" { diff --git a/modules/cluster/outputs.tf b/modules/cluster/outputs.tf index ff100d27..2c5ebfb2 100644 --- a/modules/cluster/outputs.tf +++ b/modules/cluster/outputs.tf @@ -45,6 +45,11 @@ output "cluster_capacity_providers" { ################################################################################ output "autoscaling_capacity_providers" { + description = "[DEPRECATED - use `capacity_providers`] Map of autoscaling capacity providers created and their attributes" + value = aws_ecs_capacity_provider.this +} + +output "capacity_providers" { description = "Map of autoscaling capacity providers created and their attributes" value = aws_ecs_capacity_provider.this } diff --git a/modules/cluster/variables.tf b/modules/cluster/variables.tf index c1154202..817ea13d 100644 --- a/modules/cluster/variables.tf +++ b/modules/cluster/variables.tf @@ -143,7 +143,7 @@ variable "autoscaling_capacity_providers" { variable "capacity_providers" { description = "Map of capacity provider definitions to create for the cluster" type = map(object({ - autoscaling_group_provider = optional(object({ + auto_scaling_group_provider = optional(object({ auto_scaling_group_arn = string managed_draining = optional(string, "ENABLED") managed_scaling = optional(object({ @@ -221,7 +221,7 @@ variable "capacity_providers" { storage_size_gib = number })) }) - propagate_tags = optional(string) + propagate_tags = optional(string, "CAPACITY_PROVIDER") })) name = optional(string) # Will fall back to use map key if not set tags = optional(map(string), {}) diff --git a/outputs.tf b/outputs.tf index ae9d9d35..37a9d605 100644 --- a/outputs.tf +++ b/outputs.tf @@ -33,10 +33,15 @@ output "cluster_capacity_providers" { } output "autoscaling_capacity_providers" { - description = "Map of autoscaling capacity providers created and their attributes" + description = "[DEPRECATED - use `capacity_providers`] Map of autoscaling capacity providers created and their attributes" value = module.cluster.autoscaling_capacity_providers } +output "capacity_providers" { + description = "Map of autoscaling capacity providers created and their attributes" + value = module.cluster.capacity_providers +} + output "task_exec_iam_role_name" { description = "Task execution IAM role name" value = module.cluster.task_exec_iam_role_name diff --git a/variables.tf b/variables.tf index 78caf5f1..57478167 100644 --- a/variables.tf +++ b/variables.tf @@ -148,7 +148,7 @@ variable "autoscaling_capacity_providers" { variable "capacity_providers" { description = "Map of capacity provider definitions to create for the cluster" type = map(object({ - autoscaling_group_provider = optional(object({ + auto_scaling_group_provider = optional(object({ auto_scaling_group_arn = string managed_draining = optional(string, "ENABLED") managed_scaling = optional(object({ @@ -226,7 +226,7 @@ variable "capacity_providers" { storage_size_gib = number })) }) - propagate_tags = optional(string) + propagate_tags = optional(string, "CAPACITY_PROVIDER") })) name = optional(string) # Will fall back to use map key if not set tags = optional(map(string), {}) diff --git a/wrappers/cluster/main.tf b/wrappers/cluster/main.tf index 273131a1..ce6bfae9 100644 --- a/wrappers/cluster/main.tf +++ b/wrappers/cluster/main.tf @@ -21,6 +21,7 @@ module "wrapper" { create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.defaults.create_cloudwatch_log_group, true) create_infrastructure_iam_role = try(each.value.create_infrastructure_iam_role, var.defaults.create_infrastructure_iam_role, true) create_node_iam_instance_profile = try(each.value.create_node_iam_instance_profile, var.defaults.create_node_iam_instance_profile, true) + create_security_group = try(each.value.create_security_group, var.defaults.create_security_group, true) create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, false) create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) default_capacity_provider_strategy = try(each.value.default_capacity_provider_strategy, var.defaults.default_capacity_provider_strategy, {}) @@ -42,6 +43,12 @@ module "wrapper" { node_iam_role_tags = try(each.value.node_iam_role_tags, var.defaults.node_iam_role_tags, {}) node_iam_role_use_name_prefix = try(each.value.node_iam_role_use_name_prefix, var.defaults.node_iam_role_use_name_prefix, true) region = try(each.value.region, var.defaults.region, null) + security_group_description = try(each.value.security_group_description, var.defaults.security_group_description, null) + security_group_egress_rules = try(each.value.security_group_egress_rules, var.defaults.security_group_egress_rules, {}) + security_group_ingress_rules = try(each.value.security_group_ingress_rules, var.defaults.security_group_ingress_rules, {}) + security_group_name = try(each.value.security_group_name, var.defaults.security_group_name, null) + security_group_tags = try(each.value.security_group_tags, var.defaults.security_group_tags, {}) + security_group_use_name_prefix = try(each.value.security_group_use_name_prefix, var.defaults.security_group_use_name_prefix, true) service_connect_defaults = try(each.value.service_connect_defaults, var.defaults.service_connect_defaults, null) setting = try(each.value.setting, var.defaults.setting, [ { @@ -60,4 +67,5 @@ module "wrapper" { task_exec_iam_statements = try(each.value.task_exec_iam_statements, var.defaults.task_exec_iam_statements, null) task_exec_secret_arns = try(each.value.task_exec_secret_arns, var.defaults.task_exec_secret_arns, []) task_exec_ssm_param_arns = try(each.value.task_exec_ssm_param_arns, var.defaults.task_exec_ssm_param_arns, []) + vpc_id = try(each.value.vpc_id, var.defaults.vpc_id, null) } From d88ff02068e462e604ce591e07fb2b310d96b0ed Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Fri, 24 Oct 2025 20:26:54 -0500 Subject: [PATCH 05/10] feat: Correct dependency mappings and resolved outdated assumptions --- README.md | 16 +- examples/complete/README.md | 4 +- examples/complete/main.tf | 2 +- examples/complete/versions.tf | 2 +- examples/container-definition/README.md | 2 +- examples/container-definition/versions.tf | 2 +- examples/ec2-autoscaling/README.md | 4 +- examples/ec2-autoscaling/main.tf | 4 +- examples/ec2-autoscaling/versions.tf | 2 +- examples/fargate/README.md | 4 +- examples/fargate/versions.tf | 2 +- examples/managed-instances/README.md | 21 +- examples/managed-instances/main.tf | 189 +++++++------- examples/managed-instances/versions.tf | 2 +- main.tf | 17 +- modules/cluster/README.md | 22 +- modules/cluster/main.tf | 293 ++++++++++++++++++---- modules/cluster/outputs.tf | 7 +- modules/cluster/variables.tf | 84 +++++-- modules/cluster/versions.tf | 6 +- modules/container-definition/README.md | 4 +- modules/container-definition/versions.tf | 2 +- modules/service/README.md | 4 +- modules/service/main.tf | 20 +- modules/service/versions.tf | 2 +- outputs.tf | 5 - variables.tf | 155 ++++++++++-- versions.tf | 2 +- wrappers/cluster/main.tf | 95 ++++--- wrappers/cluster/versions.tf | 6 +- wrappers/container-definition/versions.tf | 2 +- wrappers/main.tf | 98 +++++--- wrappers/service/versions.tf | 2 +- wrappers/versions.tf | 2 +- 34 files changed, 771 insertions(+), 313 deletions(-) diff --git a/README.md b/README.md index a4de671a..f48a9589 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ module "ecs" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.5.7 | -| [aws](#requirement\_aws) | >= 6.21 | +| [aws](#requirement\_aws) | >= 6.23 | ## Providers @@ -177,13 +177,13 @@ No resources. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [autoscaling\_capacity\_providers](#input\_autoscaling\_capacity\_providers) | [DEPRECATED - use `capacity_providers` instead] Map of autoscaling capacity provider definitions to create for the cluster |
map(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | | [capacity\_providers](#input\_capacity\_providers) | Map of capacity provider definitions to create for the cluster |
map(object({
auto_scaling_group_provider = optional(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
}))
managed_instances_provider = optional(object({
infrastructure_role_arn = optional(string)
instance_launch_template = object({
ec2_instance_profile_arn = optional(string)
instance_requirements = optional(object({
accelerator_count = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_manufacturers = optional(list(string))
accelerator_names = optional(list(string))
accelerator_total_memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_types = optional(list(string))
allowed_instance_types = optional(list(string))
bare_metal = optional(string)
baseline_ebs_bandwidth_mbps = optional(object({
max = optional(number)
min = optional(number)
}))
burstable_performance = optional(string)
cpu_manufacturers = optional(list(string))
excluded_instance_types = optional(list(string))
instance_generations = optional(list(string))
local_storage = optional(string)
local_storage_types = optional(list(string))
max_spot_price_as_percentage_of_optimal_on_demand_price = optional(number)
memory_gib_per_vcpu = optional(object({
max = optional(number)
min = optional(number)
}))
memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
network_bandwidth_gbps = optional(object({
max = optional(number)
min = optional(number)
}))
network_interface_count = optional(object({
max = optional(number)
min = optional(number)
}))
on_demand_max_price_percentage_over_lowest_price = optional(number)
require_hibernate_support = optional(bool)
spot_max_price_percentage_over_lowest_price = optional(number)
total_local_storage_gb = optional(object({
max = optional(number)
min = optional(number)
}))
vcpu_count = optional(object({
max = optional(number)
min = optional(number)
}))
}))
monitoring = optional(string)
network_configuration = optional(object({
security_groups = optional(list(string), [])
subnets = list(string)
}))
storage_configuration = optional(object({
storage_size_gib = number
}))
})
propagate_tags = optional(string, "CAPACITY_PROVIDER")
}))
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | | [cloudwatch\_log\_group\_class](#input\_cloudwatch\_log\_group\_class) | Specified the log class of the log group. Possible values are: `STANDARD` or `INFREQUENT_ACCESS` | `string` | `null` | no | | [cloudwatch\_log\_group\_kms\_key\_id](#input\_cloudwatch\_log\_group\_kms\_key\_id) | If a KMS Key ARN is set, this key will be used to encrypt the corresponding log group. Please be sure that the KMS Key has an appropriate key policy (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html) | `string` | `null` | no | | [cloudwatch\_log\_group\_name](#input\_cloudwatch\_log\_group\_name) | Custom name of CloudWatch Log Group for ECS cluster | `string` | `null` | no | | [cloudwatch\_log\_group\_retention\_in\_days](#input\_cloudwatch\_log\_group\_retention\_in\_days) | Number of days to retain log events | `number` | `90` | no | | [cloudwatch\_log\_group\_tags](#input\_cloudwatch\_log\_group\_tags) | A map of additional tags to add to the log group created | `map(string)` | `{}` | no | +| [cluster\_capacity\_providers](#input\_cluster\_capacity\_providers) | List of capacity provider names to associate with the ECS cluster. Note: any capacity providers created by this module will be automatically added | `list(string)` | `[]` | no | | [cluster\_configuration](#input\_cluster\_configuration) | The execute command configuration for the cluster |
object({
execute_command_configuration = optional(object({
kms_key_id = optional(string)
log_configuration = optional(object({
cloud_watch_encryption_enabled = optional(bool)
cloud_watch_log_group_name = optional(string)
s3_bucket_encryption_enabled = optional(bool)
s3_bucket_name = optional(string)
s3_kms_key_id = optional(string)
s3_key_prefix = optional(string)
}))
logging = optional(string, "OVERRIDE")
}))
managed_storage_configuration = optional(object({
fargate_ephemeral_storage_kms_key_id = optional(string)
kms_key_id = optional(string)
}))
})
|
{
"execute_command_configuration": {
"log_configuration": {
"cloud_watch_log_group_name": "placeholder"
}
}
}
| no | | [cluster\_name](#input\_cluster\_name) | Name of the cluster (up to 255 letters, numbers, hyphens, and underscores) | `string` | `""` | no | | [cluster\_service\_connect\_defaults](#input\_cluster\_service\_connect\_defaults) | Configures a default Service Connect namespace |
object({
namespace = string
})
| `null` | no | @@ -193,13 +193,17 @@ No resources. | [create\_cloudwatch\_log\_group](#input\_create\_cloudwatch\_log\_group) | Determines whether a log group is created by this module for the cluster logs. If not, AWS will automatically create one if logging is enabled | `bool` | `true` | no | | [create\_infrastructure\_iam\_role](#input\_create\_infrastructure\_iam\_role) | Determines whether the ECS infrastructure IAM role should be created | `bool` | `true` | no | | [create\_node\_iam\_instance\_profile](#input\_create\_node\_iam\_instance\_profile) | Determines whether an IAM instance profile is created or to use an existing IAM instance profile | `bool` | `true` | no | +| [create\_security\_group](#input\_create\_security\_group) | Determines if a security group is created | `bool` | `true` | no | | [create\_task\_exec\_iam\_role](#input\_create\_task\_exec\_iam\_role) | Determines whether the ECS task definition IAM role should be created | `bool` | `false` | no | | [create\_task\_exec\_policy](#input\_create\_task\_exec\_policy) | Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters | `bool` | `true` | no | | [default\_capacity\_provider\_strategy](#input\_default\_capacity\_provider\_strategy) | Map of default capacity provider strategy definitions to use for the cluster |
map(object({
base = optional(number)
name = optional(string) # Will fall back to use map key if not set
weight = optional(number)
}))
| `null` | no | | [infrastructure\_iam\_role\_description](#input\_infrastructure\_iam\_role\_description) | Description of the role | `string` | `null` | no | | [infrastructure\_iam\_role\_name](#input\_infrastructure\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | +| [infrastructure\_iam\_role\_override\_policy\_documents](#input\_infrastructure\_iam\_role\_override\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid` | `list(string)` | `[]` | no | | [infrastructure\_iam\_role\_path](#input\_infrastructure\_iam\_role\_path) | IAM role path | `string` | `null` | no | | [infrastructure\_iam\_role\_permissions\_boundary](#input\_infrastructure\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [infrastructure\_iam\_role\_source\_policy\_documents](#input\_infrastructure\_iam\_role\_source\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. Statements must have unique `sid`s | `list(string)` | `[]` | no | +| [infrastructure\_iam\_role\_statements](#input\_infrastructure\_iam\_role\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage |
map(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string, "Allow")
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
variable = string
values = list(string)
})))
}))
| `null` | no | | [infrastructure\_iam\_role\_tags](#input\_infrastructure\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | | [infrastructure\_iam\_role\_use\_name\_prefix](#input\_infrastructure\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`iam_role_name`) is used as a prefix | `bool` | `true` | no | | [node\_iam\_role\_additional\_policies](#input\_node\_iam\_role\_additional\_policies) | Additional policies to be added to the IAM role | `map(string)` | `{}` | no | @@ -213,6 +217,12 @@ No resources. | [node\_iam\_role\_tags](#input\_node\_iam\_role\_tags) | A map of additional tags to add to the IAM role/instance profile created | `map(string)` | `{}` | no | | [node\_iam\_role\_use\_name\_prefix](#input\_node\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role/instance profile name (`node_iam_role_name`) is used as a prefix | `bool` | `true` | no | | [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | +| [security\_group\_description](#input\_security\_group\_description) | Description of the security group created | `string` | `null` | no | +| [security\_group\_egress\_rules](#input\_security\_group\_egress\_rules) | Security group egress rules to add to the security group created |
map(object({
name = optional(string)

cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
}))
|
{
"all_ipv4": {
"cidr_ipv4": "0.0.0.0/0",
"description": "Allow all IPv4 traffic",
"ip_protocol": "-1"
},
"all_ipv6": {
"cidr_ipv6": "::/0",
"description": "Allow all IPv6 traffic",
"ip_protocol": "-1"
}
}
| no | +| [security\_group\_ingress\_rules](#input\_security\_group\_ingress\_rules) | Security group ingress rules to add to the security group created |
map(object({
name = optional(string)

cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
}))
| `{}` | no | +| [security\_group\_name](#input\_security\_group\_name) | Name to use on security group created | `string` | `null` | no | +| [security\_group\_tags](#input\_security\_group\_tags) | A map of additional tags to add to the security group created | `map(string)` | `{}` | no | +| [security\_group\_use\_name\_prefix](#input\_security\_group\_use\_name\_prefix) | Determines whether the security group name (`security_group_name`) is used as a prefix | `bool` | `true` | no | | [services](#input\_services) | Map of service definitions to create |
map(object({
create = optional(bool)
create_service = optional(bool)
tags = optional(map(string))

# Service
ignore_task_definition_changes = optional(bool)
alarms = optional(object({
alarm_names = list(string)
enable = optional(bool)
rollback = optional(bool)
}))
availability_zone_rebalancing = optional(string)
capacity_provider_strategy = optional(map(object({
base = optional(number)
capacity_provider = string
weight = optional(number)
})))
deployment_circuit_breaker = optional(object({
enable = bool
rollback = bool
}))
deployment_configuration = optional(object({
strategy = optional(string)
bake_time_in_minutes = optional(string)
canary_configuration = optional(object({
canary_bake_time_in_minutes = optional(string)
canary_percent = optional(string)
}))
linear_configuration = optional(object({
step_bake_time_in_minutes = optional(string)
step_percent = optional(string)
}))
lifecycle_hook = optional(map(object({
hook_target_arn = string
role_arn = string
lifecycle_stages = list(string)
hook_details = optional(string)
})))
}))
deployment_controller = optional(object({
type = optional(string)
}))
deployment_maximum_percent = optional(number, 200)
deployment_minimum_healthy_percent = optional(number, 66)
desired_count = optional(number, 1)
enable_ecs_managed_tags = optional(bool)
enable_execute_command = optional(bool)
force_delete = optional(bool)
force_new_deployment = optional(bool)
health_check_grace_period_seconds = optional(number)
launch_type = optional(string)
load_balancer = optional(map(object({
container_name = string
container_port = number
elb_name = optional(string)
target_group_arn = optional(string)
advanced_configuration = optional(object({
alternate_target_group_arn = string
production_listener_rule = string
role_arn = string
test_listener_rule = optional(string)
}))
})))
name = optional(string) # Will fall back to use map key if not set
assign_public_ip = optional(bool)
security_group_ids = optional(list(string))
subnet_ids = optional(list(string))
ordered_placement_strategy = optional(map(object({
field = optional(string)
type = string
})))
placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
platform_version = optional(string)
propagate_tags = optional(string)
scheduling_strategy = optional(string)
service_connect_configuration = optional(object({
enabled = optional(bool)
log_configuration = optional(object({
log_driver = string
options = optional(map(string))
secret_option = optional(list(object({
name = string
value_from = string
})))
}))
namespace = optional(string)
service = optional(list(object({
client_alias = optional(object({
dns_name = optional(string)
port = number
test_traffic_rules = optional(list(object({
header = optional(object({
name = string
value = object({
exact = string
})
}))
})))
}))
discovery_name = optional(string)
ingress_port_override = optional(number)
port_name = string
timeout = optional(object({
idle_timeout_seconds = optional(number)
per_request_timeout_seconds = optional(number)
}))
tls = optional(object({
issuer_cert_authority = object({
aws_pca_authority_arn = string
})
kms_key = optional(string)
role_arn = optional(string)
}))
})))
}))
service_registries = optional(object({
container_name = optional(string)
container_port = optional(number)
port = optional(number)
registry_arn = string
}))
sigint_rollback = optional(bool)
timeouts = optional(object({
create = optional(string)
update = optional(string)
delete = optional(string)
}))
triggers = optional(map(string))
volume_configuration = optional(object({
name = string
managed_ebs_volume = object({
encrypted = optional(bool)
file_system_type = optional(string)
iops = optional(number)
kms_key_id = optional(string)
size_in_gb = optional(number)
snapshot_id = optional(string)
tag_specifications = optional(list(object({
propagate_tags = optional(string)
resource_type = string
tags = optional(map(string))
})))
throughput = optional(number)
volume_type = optional(string)
})
}))
vpc_lattice_configurations = optional(object({
role_arn = string
target_group_arn = string
port_name = string
}))
wait_for_steady_state = optional(bool)
service_tags = optional(map(string))
# Service - IAM Role
create_iam_role = optional(bool)
iam_role_arn = optional(string)
iam_role_name = optional(string)
iam_role_use_name_prefix = optional(bool)
iam_role_path = optional(string)
iam_role_description = optional(string)
iam_role_permissions_boundary = optional(string)
iam_role_tags = optional(map(string))
iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
# Task Definition
create_task_definition = optional(bool)
task_definition_arn = optional(string)
container_definitions = optional(map(object({
operating_system_family = optional(string)
tags = optional(map(string))

# Container definition
command = optional(list(string))
cpu = optional(number)
credentialSpecs = optional(list(string))
dependsOn = optional(list(object({
condition = string
containerName = string
})))
disableNetworking = optional(bool)
dnsSearchDomains = optional(list(string))
dnsServers = optional(list(string))
dockerLabels = optional(map(string))
dockerSecurityOptions = optional(list(string))
enable_execute_command = optional(bool)
entrypoint = optional(list(string))
environment = optional(list(object({
name = string
value = string
})))
environmentFiles = optional(list(object({
type = string
value = string
})))
essential = optional(bool)
extraHosts = optional(list(object({
hostname = string
ipAddress = string
})))
firelensConfiguration = optional(object({
options = optional(map(string))
type = optional(string)
}))
healthCheck = optional(object({
command = optional(list(string))
interval = optional(number)
retries = optional(number)
startPeriod = optional(number)
timeout = optional(number)
}))
hostname = optional(string)
image = optional(string)
interactive = optional(bool)
links = optional(list(string))
linuxParameters = optional(object({
capabilities = optional(object({
add = optional(list(string))
drop = optional(list(string))
}))
devices = optional(list(object({
containerPath = optional(string)
hostPath = optional(string)
permissions = optional(list(string))
})))
initProcessEnabled = optional(bool)
maxSwap = optional(number)
sharedMemorySize = optional(number)
swappiness = optional(number)
tmpfs = optional(list(object({
containerPath = string
mountOptions = optional(list(string))
size = number
})))
}))
logConfiguration = optional(object({
logDriver = optional(string)
options = optional(map(string))
secretOptions = optional(list(object({
name = string
valueFrom = string
})))
}))
memory = optional(number)
memoryReservation = optional(number)
mountPoints = optional(list(object({
containerPath = optional(string)
readOnly = optional(bool)
sourceVolume = optional(string)
})), [])
name = optional(string)
portMappings = optional(list(object({
appProtocol = optional(string)
containerPort = optional(number)
containerPortRange = optional(string)
hostPort = optional(number)
name = optional(string)
protocol = optional(string)
})), [])
privileged = optional(bool)
pseudoTerminal = optional(bool)
readonlyRootFilesystem = optional(bool)
repositoryCredentials = optional(object({
credentialsParameter = optional(string)
}))
resourceRequirements = optional(list(object({
type = string
value = string
})))
restartPolicy = optional(object({
enabled = optional(bool)
ignoredExitCodes = optional(list(number))
restartAttemptPeriod = optional(number)
}))
secrets = optional(list(object({
name = string
valueFrom = string
})))
startTimeout = optional(number)
stopTimeout = optional(number)
systemControls = optional(list(object({
namespace = optional(string)
value = optional(string)
})))
ulimits = optional(list(object({
hardLimit = number
name = string
softLimit = number
})))
user = optional(string)
versionConsistency = optional(string)
volumesFrom = optional(list(object({
readOnly = optional(bool)
sourceContainer = optional(string)
})))
workingDirectory = optional(string)

# Cloudwatch Log Group
service = optional(string, "")
enable_cloudwatch_logging = optional(bool)
create_cloudwatch_log_group = optional(bool)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool)
cloudwatch_log_group_class = optional(string)
cloudwatch_log_group_retention_in_days = optional(number)
cloudwatch_log_group_kms_key_id = optional(string)
})))
cpu = optional(number, 1024)
enable_fault_injection = optional(bool)
ephemeral_storage = optional(object({
size_in_gib = number
}))
family = optional(string)
ipc_mode = optional(string)
memory = optional(number, 2048)
network_mode = optional(string)
pid_mode = optional(string)
proxy_configuration = optional(object({
container_name = string
properties = optional(map(string))
type = optional(string)
}))
requires_compatibilities = optional(list(string))
runtime_platform = optional(object({
cpu_architecture = optional(string)
operating_system_family = optional(string)
}))
skip_destroy = optional(bool)
task_definition_placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
track_latest = optional(bool)
volume = optional(map(object({
configure_at_launch = optional(bool)
docker_volume_configuration = optional(object({
autoprovision = optional(bool)
driver = optional(string)
driver_opts = optional(map(string))
labels = optional(map(string))
scope = optional(string)
}))
efs_volume_configuration = optional(object({
authorization_config = optional(object({
access_point_id = optional(string)
iam = optional(string)
}))
file_system_id = string
root_directory = optional(string)
transit_encryption = optional(string)
transit_encryption_port = optional(number)
}))
fsx_windows_file_server_volume_configuration = optional(object({
authorization_config = optional(object({
credentials_parameter = string
domain = string
}))
file_system_id = string
root_directory = string
}))
host_path = optional(string)
name = optional(string)
})))
task_tags = optional(map(string))
# Task Execution - IAM Role
create_task_exec_iam_role = optional(bool)
task_exec_iam_role_arn = optional(string)
task_exec_iam_role_name = optional(string)
task_exec_iam_role_use_name_prefix = optional(bool)
task_exec_iam_role_path = optional(string)
task_exec_iam_role_description = optional(string)
task_exec_iam_role_permissions_boundary = optional(string)
task_exec_iam_role_tags = optional(map(string))
task_exec_iam_role_policies = optional(map(string))
task_exec_iam_role_max_session_duration = optional(number)
create_task_exec_policy = optional(bool)
task_exec_ssm_param_arns = optional(list(string))
task_exec_secret_arns = optional(list(string))
task_exec_iam_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
task_exec_iam_policy_path = optional(string)
# Tasks - IAM Role
create_tasks_iam_role = optional(bool)
tasks_iam_role_arn = optional(string)
tasks_iam_role_name = optional(string)
tasks_iam_role_use_name_prefix = optional(bool)
tasks_iam_role_path = optional(string)
tasks_iam_role_description = optional(string)
tasks_iam_role_permissions_boundary = optional(string)
tasks_iam_role_tags = optional(map(string))
tasks_iam_role_policies = optional(map(string))
tasks_iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
# Task Set
external_id = optional(string)
scale = optional(object({
unit = optional(string)
value = optional(number)
}))
wait_until_stable = optional(bool)
wait_until_stable_timeout = optional(string)
# Autoscaling
enable_autoscaling = optional(bool)
autoscaling_min_capacity = optional(number)
autoscaling_max_capacity = optional(number)
autoscaling_policies = optional(map(object({
name = optional(string) # Will fall back to the key name if not provided
policy_type = optional(string)
predictive_scaling_policy_configuration = optional(object({
max_capacity_breach_behavior = optional(string)
max_capacity_buffer = optional(number)
metric_specification = list(object({
customized_capacity_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_load_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_scaling_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
predefined_load_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_metric_pair_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_scaling_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
target_value = number
}))
mode = optional(string)
scheduling_buffer_time = optional(number)
}))
step_scaling_policy_configuration = optional(object({
adjustment_type = optional(string)
cooldown = optional(number)
metric_aggregation_type = optional(string)
min_adjustment_magnitude = optional(number)
step_adjustment = optional(list(object({
metric_interval_lower_bound = optional(string)
metric_interval_upper_bound = optional(string)
scaling_adjustment = number
})))
}))
target_tracking_scaling_policy_configuration = optional(object({
customized_metric_specification = optional(object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
metrics = optional(list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = string
namespace = string
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
})))
namespace = optional(string)
statistic = optional(string)
unit = optional(string)
}))

disable_scale_in = optional(bool)
predefined_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
scale_in_cooldown = optional(number)
scale_out_cooldown = optional(number)
target_value = optional(number)
}))
})))
autoscaling_scheduled_actions = optional(map(object({
name = optional(string)
min_capacity = number
max_capacity = number
schedule = string
start_time = optional(string)
end_time = optional(string)
timezone = optional(string)
})))
autoscaling_suspended_state = optional(object({
dynamic_scaling_in_suspended = optional(bool)
dynamic_scaling_out_suspended = optional(bool)
scheduled_scaling_suspended = optional(bool)
}))
# Security Group
create_security_group = optional(bool)
vpc_id = optional(string)
security_group_name = optional(string)
security_group_use_name_prefix = optional(bool)
security_group_description = optional(string)
security_group_ingress_rules = optional(map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string))
to_port = optional(string)
})))
security_group_egress_rules = optional(map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string))
to_port = optional(string)
})))
security_group_tags = optional(map(string))
# ECS Infrastructure IAM Role
create_infrastructure_iam_role = optional(bool)
infrastructure_iam_role_arn = optional(string)
infrastructure_iam_role_name = optional(string)
infrastructure_iam_role_use_name_prefix = optional(bool)
infrastructure_iam_role_path = optional(string)
infrastructure_iam_role_description = optional(string)
infrastructure_iam_role_permissions_boundary = optional(string)
infrastructure_iam_role_tags = optional(map(string))
}))
| `null` | no | | [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | | [task\_exec\_iam\_role\_description](#input\_task\_exec\_iam\_role\_description) | Description of the role | `string` | `null` | no | @@ -225,12 +235,12 @@ No resources. | [task\_exec\_iam\_statements](#input\_task\_exec\_iam\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage |
map(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string, "Allow")
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
variable = string
values = list(string)
})))
}))
| `null` | no | | [task\_exec\_secret\_arns](#input\_task\_exec\_secret\_arns) | List of SecretsManager secret ARNs the task execution role will be permitted to get/read | `list(string)` | `[]` | no | | [task\_exec\_ssm\_param\_arns](#input\_task\_exec\_ssm\_param\_arns) | List of SSM parameter ARNs the task execution role will be permitted to get/read | `list(string)` | `[]` | no | +| [vpc\_id](#input\_vpc\_id) | The ID of the VPC where the security group will be created | `string` | `null` | no | ## Outputs | Name | Description | |------|-------------| -| [autoscaling\_capacity\_providers](#output\_autoscaling\_capacity\_providers) | [DEPRECATED - use `capacity_providers`] Map of autoscaling capacity providers created and their attributes | | [capacity\_providers](#output\_capacity\_providers) | Map of autoscaling capacity providers created and their attributes | | [cloudwatch\_log\_group\_arn](#output\_cloudwatch\_log\_group\_arn) | ARN of CloudWatch log group created | | [cloudwatch\_log\_group\_name](#output\_cloudwatch\_log\_group\_name) | Name of CloudWatch log group created | diff --git a/examples/complete/README.md b/examples/complete/README.md index 2b87ca8b..9db4a04f 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -27,13 +27,13 @@ Note that this example may create resources which will incur monetary charges on | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.5.7 | -| [aws](#requirement\_aws) | >= 6.21 | +| [aws](#requirement\_aws) | >= 6.23 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 6.21 | +| [aws](#provider\_aws) | >= 6.23 | ## Modules diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 94a2a764..6e021cd8 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -47,7 +47,7 @@ module "ecs" { } } - autoscaling_capacity_providers = { + capacity_providers = { ASG = { auto_scaling_group_arn = module.autoscaling.autoscaling_group_arn managed_draining = "ENABLED" diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index 70f23a44..7e5b918f 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.21" + version = ">= 6.23" } } } diff --git a/examples/container-definition/README.md b/examples/container-definition/README.md index 15e50969..21380ee9 100644 --- a/examples/container-definition/README.md +++ b/examples/container-definition/README.md @@ -22,7 +22,7 @@ Note that this example may create resources which will incur monetary charges on | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.5.7 | -| [aws](#requirement\_aws) | >= 6.21 | +| [aws](#requirement\_aws) | >= 6.23 | | [null](#requirement\_null) | >= 3.2 | ## Providers diff --git a/examples/container-definition/versions.tf b/examples/container-definition/versions.tf index 42207f11..ff39d329 100644 --- a/examples/container-definition/versions.tf +++ b/examples/container-definition/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.21" + version = ">= 6.23" } null = { source = "hashicorp/null" diff --git a/examples/ec2-autoscaling/README.md b/examples/ec2-autoscaling/README.md index 756e5367..e570f615 100644 --- a/examples/ec2-autoscaling/README.md +++ b/examples/ec2-autoscaling/README.md @@ -27,13 +27,13 @@ Note that this example may create resources which will incur monetary charges on | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.5.7 | -| [aws](#requirement\_aws) | >= 6.21 | +| [aws](#requirement\_aws) | >= 6.23 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 6.21 | +| [aws](#provider\_aws) | >= 6.23 | ## Modules diff --git a/examples/ec2-autoscaling/main.tf b/examples/ec2-autoscaling/main.tf index 454b6523..0d7c2187 100644 --- a/examples/ec2-autoscaling/main.tf +++ b/examples/ec2-autoscaling/main.tf @@ -47,7 +47,7 @@ module "ecs_cluster" { } } - autoscaling_capacity_providers = { + capacity_providers = { # On-demand instances ex_1 = { auto_scaling_group_arn = module.autoscaling["ex_1"].autoscaling_group_arn @@ -120,7 +120,7 @@ module "ecs_service" { capacity_provider_strategy = { # On-demand instances ex_1 = { - capacity_provider = module.ecs_cluster.autoscaling_capacity_providers["ex_1"].name + capacity_provider = module.ecs_cluster.capacity_providers["ex_1"].name weight = 1 base = 1 } diff --git a/examples/ec2-autoscaling/versions.tf b/examples/ec2-autoscaling/versions.tf index 70f23a44..7e5b918f 100644 --- a/examples/ec2-autoscaling/versions.tf +++ b/examples/ec2-autoscaling/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.21" + version = ">= 6.23" } } } diff --git a/examples/fargate/README.md b/examples/fargate/README.md index 65ae93a7..f8cdeb51 100644 --- a/examples/fargate/README.md +++ b/examples/fargate/README.md @@ -27,13 +27,13 @@ Note that this example may create resources which will incur monetary charges on | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.5.7 | -| [aws](#requirement\_aws) | >= 6.21 | +| [aws](#requirement\_aws) | >= 6.23 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 6.21 | +| [aws](#provider\_aws) | >= 6.23 | ## Modules diff --git a/examples/fargate/versions.tf b/examples/fargate/versions.tf index 70f23a44..7e5b918f 100644 --- a/examples/fargate/versions.tf +++ b/examples/fargate/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.21" + version = ">= 6.23" } } } diff --git a/examples/managed-instances/README.md b/examples/managed-instances/README.md index dfaed040..f2b4d079 100644 --- a/examples/managed-instances/README.md +++ b/examples/managed-instances/README.md @@ -4,7 +4,7 @@ Configuration in this directory creates: - ECS cluster using ECS Managed Instances capacity provider - Example ECS service that utilizes - - AWS Firelens using FluentBit sidecar container definition + - AWS FireLens using FluentBit sidecar container definition - Service connect configuration - Load balancer target group attachment - Security group for access to the example service @@ -13,32 +13,47 @@ Configuration in this directory creates: To run this example you need to execute: +> [!CAUTION] +> Due to the ECS managed instances API, it appears that you need network connectivity quite early in the creation process and therefore also quite late in the deletion process. Therefore, for this example, a two step apply is necessary to create/destroy the resources successfully. The error you will see during creation if you do not follow this process is as follows: +> +> `Error: creating ECS Capacity Provider (mi-example): operation error ECS: CreateCapacityProvider, https response error StatusCode: 400, RequestID: 112ee8fc-7ffe-4f83-ae13-cb3a2efdb1c8, ClientException: Caught ServiceAccessDeniedException for ECSInfrastructureRole[arn:aws:iam::00000000000:role/ex-managed-instances-infra-2025121618163874450000000b] +> +> During deletion, you will see the process hang and timeout and notice that the managed instance agent is unable to connect and therefore unable to drain. +> If you create your network resources in a separate workspace/statefile (which you should!), it is unlikely you will face these issues. + ```bash terraform init terraform plan +terraform apply -target=module.vpc # to ensure NAT Gateway is created to allow network access quite early terraform apply ``` Note that this example may create resources which will incur monetary charges on your AWS bill. Run `terraform destroy` when you no longer need these resources. +```bash +terraform destroy -target=module.ecs_service -target=module.ecs_cluster +terraform destroy +``` + ## Requirements | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.5.7 | -| [aws](#requirement\_aws) | >= 6.15 | +| [aws](#requirement\_aws) | >= 6.23 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 6.15 | +| [aws](#provider\_aws) | >= 6.23 | ## Modules | Name | Source | Version | |------|--------|---------| +| [alb](#module\_alb) | terraform-aws-modules/alb/aws | ~> 10.0 | | [ecs\_cluster](#module\_ecs\_cluster) | ../../modules/cluster | n/a | | [ecs\_service](#module\_ecs\_service) | ../../modules/service | n/a | | [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 6.0 | diff --git a/examples/managed-instances/main.tf b/examples/managed-instances/main.tf index a838257f..306885bc 100644 --- a/examples/managed-instances/main.tf +++ b/examples/managed-instances/main.tf @@ -51,7 +51,6 @@ module "ecs_cluster" { network_configuration = { subnets = module.vpc.private_subnets - # security_groups = [aws_security_group.example.id] } storage_configuration = { @@ -65,10 +64,16 @@ module "ecs_cluster" { # Managed instances security group vpc_id = module.vpc.vpc_id security_group_ingress_rules = { - vpc = { - cidr_ipv4 = module.vpc.vpc_cidr_block - from_port = local.container_port - to_port = local.container_port + alb_http = { + from_port = local.container_port + description = "Service port" + referenced_security_group_id = module.alb.security_group_id + } + } + security_group_egress_rules = { + all = { + cidr_ipv4 = "0.0.0.0/0" + ip_protocol = "-1" } } @@ -93,11 +98,16 @@ module "ecs_service" { # Container definition(s) container_definitions = { (local.container_name) = { - essential = true - image = "public.ecr.aws/docker/library/httpd:latest" + image = "public.ecr.aws/docker/library/httpd:latest" + + essential = true + entrypoint = ["sh", "-c"] + command = ["/bin/sh -c \"echo 'Amazon ECS Sample App

Amazon ECS Sample App

Congratulations!

Your application is now running on a container in Amazon ECS using Amazon ECS Managed Instances.

' > /usr/local/apache2/htdocs/index.html && httpd-foreground\""] + + cpu = 256 + memory = 512 - cpu = 1024 - memory = 2048 + readonlyRootFilesystem = false portMappings = [ { @@ -110,30 +120,27 @@ module "ecs_service" { } } - # TODO - this shouldn't be required capacity_provider_strategy = { # On-demand instances mi-example = { capacity_provider = module.ecs_cluster.capacity_providers["mi-example"].name - weight = 1 - base = 1 } } - # load_balancer = { - # service = { - # target_group_arn = module.alb.target_groups["ex_ecs"].arn - # container_name = local.container_name - # container_port = local.container_port - # } - # } + load_balancer = { + service = { + target_group_arn = module.alb.target_groups["ex_ecs"].arn + container_name = local.container_name + container_port = local.container_port + } + } subnet_ids = module.vpc.private_subnets security_group_ingress_rules = { alb_http = { - from_port = local.container_port - description = "Service port" - cidr_ipv4 = module.vpc.vpc_cidr_block + from_port = local.container_port + description = "Service port" + referenced_security_group_id = module.alb.security_group_id } } @@ -144,75 +151,75 @@ module "ecs_service" { # Supporting Resources ################################################################################ -# module "alb" { -# source = "terraform-aws-modules/alb/aws" -# version = "~> 10.0" - -# name = local.name - -# load_balancer_type = "application" - -# vpc_id = module.vpc.vpc_id -# subnets = module.vpc.public_subnets - -# # For example only -# enable_deletion_protection = false - -# # Security Group -# security_group_ingress_rules = { -# all_http = { -# from_port = 80 -# to_port = 80 -# ip_protocol = "tcp" -# cidr_ipv4 = "0.0.0.0/0" -# } -# } -# security_group_egress_rules = { -# all = { -# ip_protocol = "-1" -# cidr_ipv4 = module.vpc.vpc_cidr_block -# } -# } - -# listeners = { -# ex_http = { -# port = 80 -# protocol = "HTTP" - -# forward = { -# target_group_key = "ex_ecs" -# } -# } -# } - -# target_groups = { -# ex_ecs = { -# backend_protocol = "HTTP" -# backend_port = local.container_port -# target_type = "ip" -# deregistration_delay = 5 -# load_balancing_cross_zone_enabled = true - -# health_check = { -# enabled = true -# healthy_threshold = 5 -# interval = 30 -# matcher = "200" -# path = "/" -# port = "traffic-port" -# protocol = "HTTP" -# timeout = 5 -# unhealthy_threshold = 2 -# } - -# # Theres nothing to attach here in this definition. Instead, -# # ECS will attach the IPs of the tasks to this target group -# create_attachment = false -# } -# } - -# tags = local.tags -# } +module "alb" { + source = "terraform-aws-modules/alb/aws" + version = "~> 10.0" + + name = local.name + + load_balancer_type = "application" + + vpc_id = module.vpc.vpc_id + subnets = module.vpc.public_subnets + + # For example only + enable_deletion_protection = false + + # Security Group + security_group_ingress_rules = { + all_http = { + from_port = local.container_port + to_port = local.container_port + ip_protocol = "tcp" + cidr_ipv4 = "0.0.0.0/0" + } + } + security_group_egress_rules = { + all = { + ip_protocol = "-1" + cidr_ipv4 = module.vpc.vpc_cidr_block + } + } + + listeners = { + ex_http = { + port = local.container_port + protocol = "HTTP" + + forward = { + target_group_key = "ex_ecs" + } + } + } + + target_groups = { + ex_ecs = { + backend_protocol = "HTTP" + backend_port = local.container_port + target_type = "ip" + deregistration_delay = 5 + load_balancing_cross_zone_enabled = true + + health_check = { + enabled = true + healthy_threshold = 5 + interval = 30 + matcher = "200" + path = "/" + port = "traffic-port" + protocol = "HTTP" + timeout = 5 + unhealthy_threshold = 2 + } + + # Theres nothing to attach here in this definition. Instead, + # ECS will attach the IPs of the tasks to this target group + create_attachment = false + } + } + + tags = local.tags +} module "vpc" { source = "terraform-aws-modules/vpc/aws" diff --git a/examples/managed-instances/versions.tf b/examples/managed-instances/versions.tf index e1ac31ca..7e5b918f 100644 --- a/examples/managed-instances/versions.tf +++ b/examples/managed-instances/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.15" + version = ">= 6.23" } } } diff --git a/main.tf b/main.tf index 8d605fdc..7a2c729d 100644 --- a/main.tf +++ b/main.tf @@ -23,7 +23,7 @@ module "cluster" { cloudwatch_log_group_tags = var.cloudwatch_log_group_tags # Cluster capacity providers - autoscaling_capacity_providers = var.autoscaling_capacity_providers + cluster_capacity_providers = var.cluster_capacity_providers capacity_providers = var.capacity_providers default_capacity_provider_strategy = var.default_capacity_provider_strategy @@ -54,6 +54,11 @@ module "cluster" { infrastructure_iam_role_permissions_boundary = var.infrastructure_iam_role_permissions_boundary infrastructure_iam_role_tags = var.infrastructure_iam_role_tags + # Infrastructure IAM role policy + infrastructure_iam_role_source_policy_documents = var.infrastructure_iam_role_source_policy_documents + infrastructure_iam_role_override_policy_documents = var.infrastructure_iam_role_override_policy_documents + infrastructure_iam_role_statements = var.infrastructure_iam_role_statements + # Node IAM role & instance profile create_node_iam_instance_profile = var.create_node_iam_instance_profile node_iam_role_name = var.node_iam_role_name @@ -69,6 +74,16 @@ module "cluster" { node_iam_role_override_policy_documents = var.node_iam_role_override_policy_documents node_iam_role_statements = var.node_iam_role_statements + # Security Group + create_security_group = var.create_security_group + vpc_id = var.vpc_id + security_group_name = var.security_group_name + security_group_use_name_prefix = var.security_group_use_name_prefix + security_group_description = var.security_group_description + security_group_ingress_rules = var.security_group_ingress_rules + security_group_egress_rules = var.security_group_egress_rules + security_group_tags = var.security_group_tags + tags = merge(var.tags, var.cluster_tags) } diff --git a/modules/cluster/README.md b/modules/cluster/README.md index 576fc6ef..1c4bb6c7 100644 --- a/modules/cluster/README.md +++ b/modules/cluster/README.md @@ -135,13 +135,15 @@ module "ecs_cluster" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.5.7 | -| [aws](#requirement\_aws) | >= 6.21 | +| [aws](#requirement\_aws) | >= 6.23 | +| [time](#requirement\_time) | >= 0.13 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 6.21 | +| [aws](#provider\_aws) | >= 6.23 | +| [time](#provider\_time) | >= 0.13 | ## Modules @@ -156,12 +158,13 @@ No modules. | [aws_ecs_cluster.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster) | resource | | [aws_ecs_cluster_capacity_providers.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster_capacity_providers) | resource | | [aws_iam_instance_profile.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | +| [aws_iam_policy.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_policy.node](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_policy.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.node](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | -| [aws_iam_role_policy_attachment.infrastructure_managed_instances](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.node](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.node_additional](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | @@ -169,8 +172,10 @@ No modules. | [aws_security_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_vpc_security_group_egress_rule.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | | [aws_vpc_security_group_ingress_rule.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | +| [time_sleep.this](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy_document.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.infrastructure_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.node](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.node_assume_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -182,13 +187,14 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [autoscaling\_capacity\_providers](#input\_autoscaling\_capacity\_providers) | [DEPRECATED - use `capacity_providers` instead] Map of autoscaling capacity provider definitions to create for the cluster |
map(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | -| [capacity\_providers](#input\_capacity\_providers) | Map of capacity provider definitions to create for the cluster |
map(object({
auto_scaling_group_provider = optional(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
}))
managed_instances_provider = optional(object({
infrastructure_role_arn = optional(string)
instance_launch_template = object({
ec2_instance_profile_arn = optional(string)
instance_requirements = optional(object({
accelerator_count = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_manufacturers = optional(list(string))
accelerator_names = optional(list(string))
accelerator_total_memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_types = optional(list(string))
allowed_instance_types = optional(list(string))
bare_metal = optional(string)
baseline_ebs_bandwidth_mbps = optional(object({
max = optional(number)
min = optional(number)
}))
burstable_performance = optional(string)
cpu_manufacturers = optional(list(string))
excluded_instance_types = optional(list(string))
instance_generations = optional(list(string))
local_storage = optional(string)
local_storage_types = optional(list(string))
max_spot_price_as_percentage_of_optimal_on_demand_price = optional(number)
memory_gib_per_vcpu = optional(object({
max = optional(number)
min = optional(number)
}))
memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
network_bandwidth_gbps = optional(object({
max = optional(number)
min = optional(number)
}))
network_interface_count = optional(object({
max = optional(number)
min = optional(number)
}))
on_demand_max_price_percentage_over_lowest_price = optional(number)
require_hibernate_support = optional(bool)
spot_max_price_percentage_over_lowest_price = optional(number)
total_local_storage_gb = optional(object({
max = optional(number)
min = optional(number)
}))
vcpu_count = optional(object({
max = optional(number)
min = optional(number)
}))
}))
monitoring = optional(string)
network_configuration = optional(object({
security_groups = optional(list(string), [])
subnets = list(string)
}))
storage_configuration = optional(object({
storage_size_gib = number
}))
})
propagate_tags = optional(string, "CAPACITY_PROVIDER")
}))
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | +| [capacity\_providers](#input\_capacity\_providers) | Map of capacity provider definitions to create |
map(object({
auto_scaling_group_provider = optional(object({
auto_scaling_group_arn = string
managed_draining = optional(string, "ENABLED")
managed_scaling = optional(object({
instance_warmup_period = optional(number)
maximum_scaling_step_size = optional(number)
minimum_scaling_step_size = optional(number)
status = optional(string)
target_capacity = optional(number)
}))
managed_termination_protection = optional(string)
}))
managed_instances_provider = optional(object({
infrastructure_role_arn = optional(string)
instance_launch_template = object({
ec2_instance_profile_arn = optional(string)
instance_requirements = optional(object({
accelerator_count = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_manufacturers = optional(list(string))
accelerator_names = optional(list(string))
accelerator_total_memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
accelerator_types = optional(list(string))
allowed_instance_types = optional(list(string))
bare_metal = optional(string)
baseline_ebs_bandwidth_mbps = optional(object({
max = optional(number)
min = optional(number)
}))
burstable_performance = optional(string)
cpu_manufacturers = optional(list(string))
excluded_instance_types = optional(list(string))
instance_generations = optional(list(string))
local_storage = optional(string)
local_storage_types = optional(list(string))
max_spot_price_as_percentage_of_optimal_on_demand_price = optional(number)
memory_gib_per_vcpu = optional(object({
max = optional(number)
min = optional(number)
}))
memory_mib = optional(object({
max = optional(number)
min = optional(number)
}))
network_bandwidth_gbps = optional(object({
max = optional(number)
min = optional(number)
}))
network_interface_count = optional(object({
max = optional(number)
min = optional(number)
}))
on_demand_max_price_percentage_over_lowest_price = optional(number)
require_hibernate_support = optional(bool)
spot_max_price_percentage_over_lowest_price = optional(number)
total_local_storage_gb = optional(object({
max = optional(number)
min = optional(number)
}))
vcpu_count = optional(object({
max = optional(number)
min = optional(number)
}))
}))
monitoring = optional(string)
network_configuration = optional(object({
security_groups = optional(list(string), [])
subnets = list(string)
}))
storage_configuration = optional(object({
storage_size_gib = number
}))
})
propagate_tags = optional(string, "CAPACITY_PROVIDER")
}))
name = optional(string) # Will fall back to use map key if not set
tags = optional(map(string), {})
}))
| `null` | no | | [cloudwatch\_log\_group\_class](#input\_cloudwatch\_log\_group\_class) | Specified the log class of the log group. Possible values are: `STANDARD` or `INFREQUENT_ACCESS` | `string` | `null` | no | | [cloudwatch\_log\_group\_kms\_key\_id](#input\_cloudwatch\_log\_group\_kms\_key\_id) | If a KMS Key ARN is set, this key will be used to encrypt the corresponding log group. Please be sure that the KMS Key has an appropriate key policy (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html) | `string` | `null` | no | | [cloudwatch\_log\_group\_name](#input\_cloudwatch\_log\_group\_name) | Custom name of CloudWatch Log Group for ECS cluster | `string` | `null` | no | | [cloudwatch\_log\_group\_retention\_in\_days](#input\_cloudwatch\_log\_group\_retention\_in\_days) | Number of days to retain log events | `number` | `90` | no | | [cloudwatch\_log\_group\_tags](#input\_cloudwatch\_log\_group\_tags) | A map of additional tags to add to the log group created | `map(string)` | `{}` | no | +| [cluster\_capacity\_providers](#input\_cluster\_capacity\_providers) | List of capacity provider names to associate with the ECS cluster. Note: any capacity providers created by this module will be automatically added | `list(string)` | `[]` | no | +| [cluster\_capacity\_providers\_wait\_duration](#input\_cluster\_capacity\_providers\_wait\_duration) | Duration to wait after the ECS cluster has become active before attaching the cluster capacity providers | `string` | `"20s"` | no | | [configuration](#input\_configuration) | The execute command configuration for the cluster |
object({
execute_command_configuration = optional(object({
kms_key_id = optional(string)
log_configuration = optional(object({
cloud_watch_encryption_enabled = optional(bool)
cloud_watch_log_group_name = optional(string)
s3_bucket_encryption_enabled = optional(bool)
s3_bucket_name = optional(string)
s3_kms_key_id = optional(string)
s3_key_prefix = optional(string)
}))
logging = optional(string, "OVERRIDE")
}))
managed_storage_configuration = optional(object({
fargate_ephemeral_storage_kms_key_id = optional(string)
kms_key_id = optional(string)
}))
})
|
{
"execute_command_configuration": {
"log_configuration": {
"cloud_watch_log_group_name": "placeholder"
}
}
}
| no | | [create](#input\_create) | Determines whether resources will be created (affects all resources) | `bool` | `true` | no | | [create\_cloudwatch\_log\_group](#input\_create\_cloudwatch\_log\_group) | Determines whether a log group is created by this module for the cluster logs. If not, AWS will automatically create one if logging is enabled | `bool` | `true` | no | @@ -200,8 +206,11 @@ No modules. | [default\_capacity\_provider\_strategy](#input\_default\_capacity\_provider\_strategy) | Map of default capacity provider strategy definitions to use for the cluster |
map(object({
base = optional(number)
name = optional(string) # Will fall back to use map key if not set
weight = optional(number)
}))
| `{}` | no | | [infrastructure\_iam\_role\_description](#input\_infrastructure\_iam\_role\_description) | Description of the role | `string` | `null` | no | | [infrastructure\_iam\_role\_name](#input\_infrastructure\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | +| [infrastructure\_iam\_role\_override\_policy\_documents](#input\_infrastructure\_iam\_role\_override\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid` | `list(string)` | `[]` | no | | [infrastructure\_iam\_role\_path](#input\_infrastructure\_iam\_role\_path) | IAM role path | `string` | `null` | no | | [infrastructure\_iam\_role\_permissions\_boundary](#input\_infrastructure\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [infrastructure\_iam\_role\_source\_policy\_documents](#input\_infrastructure\_iam\_role\_source\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. Statements must have unique `sid`s | `list(string)` | `[]` | no | +| [infrastructure\_iam\_role\_statements](#input\_infrastructure\_iam\_role\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage |
map(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string, "Allow")
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
variable = string
values = list(string)
})))
}))
| `null` | no | | [infrastructure\_iam\_role\_tags](#input\_infrastructure\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | | [infrastructure\_iam\_role\_use\_name\_prefix](#input\_infrastructure\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`iam_role_name`) is used as a prefix | `bool` | `true` | no | | [name](#input\_name) | Name of the cluster (up to 255 letters, numbers, hyphens, and underscores) | `string` | `""` | no | @@ -217,7 +226,7 @@ No modules. | [node\_iam\_role\_use\_name\_prefix](#input\_node\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role/instance profile name (`node_iam_role_name`) is used as a prefix | `bool` | `true` | no | | [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | | [security\_group\_description](#input\_security\_group\_description) | Description of the security group created | `string` | `null` | no | -| [security\_group\_egress\_rules](#input\_security\_group\_egress\_rules) | Security group egress rules to add to the security group created |
map(object({
name = optional(string)

cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
}))
| `{}` | no | +| [security\_group\_egress\_rules](#input\_security\_group\_egress\_rules) | Security group egress rules to add to the security group created |
map(object({
name = optional(string)

cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
}))
|
{
"all_ipv4": {
"cidr_ipv4": "0.0.0.0/0",
"description": "Allow all IPv4 traffic",
"ip_protocol": "-1"
},
"all_ipv6": {
"cidr_ipv6": "::/0",
"description": "Allow all IPv6 traffic",
"ip_protocol": "-1"
}
}
| no | | [security\_group\_ingress\_rules](#input\_security\_group\_ingress\_rules) | Security group ingress rules to add to the security group created |
map(object({
name = optional(string)

cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
}))
| `{}` | no | | [security\_group\_name](#input\_security\_group\_name) | Name to use on security group created | `string` | `null` | no | | [security\_group\_tags](#input\_security\_group\_tags) | A map of additional tags to add to the security group created | `map(string)` | `{}` | no | @@ -242,7 +251,6 @@ No modules. | Name | Description | |------|-------------| | [arn](#output\_arn) | ARN that identifies the cluster | -| [autoscaling\_capacity\_providers](#output\_autoscaling\_capacity\_providers) | [DEPRECATED - use `capacity_providers`] Map of autoscaling capacity providers created and their attributes | | [capacity\_providers](#output\_capacity\_providers) | Map of autoscaling capacity providers created and their attributes | | [cloudwatch\_log\_group\_arn](#output\_cloudwatch\_log\_group\_arn) | ARN of CloudWatch log group created | | [cloudwatch\_log\_group\_name](#output\_cloudwatch\_log\_group\_name) | Name of CloudWatch log group created | diff --git a/modules/cluster/main.tf b/modules/cluster/main.tf index 0971fe85..67ba104d 100644 --- a/modules/cluster/main.tf +++ b/modules/cluster/main.tf @@ -113,10 +113,20 @@ resource "aws_cloudwatch_log_group" "this" { # Cluster Capacity Providers ################################################################################ -locals { - # TODO - is this correct?! - # Only the AutoScaling group capacity providers need to be associated with the cluster - auto_scaling_group_providers = local.capacity_providers != null ? [for k, v in local.capacity_providers : try(coalesce(v.name, k)) if v.auto_scaling_group_provider != null] : [] +# The managed instance capacity provider returns quickly in a `CREATING` state, +# but we need to wait for it to be in the `ACTIVE` state before associating it with the cluster. +resource "time_sleep" "this" { + count = var.create ? 1 : 0 + + create_duration = var.cluster_capacity_providers_wait_duration + + triggers = { + # Triggers wants a string so we have to do some cheap serialization/deserialization to transport correctly + capacity_provider_names = var.capacity_providers != null ? join(",", [for k, v in var.capacity_providers : aws_ecs_capacity_provider.this[k].name]) : "" + # This is done so that the output of `capacity_providers` also waits for them to be `ACTIVE` + # for the scenarios where users define separate cluster and service modules (serivce needs the provider to be ACTIVE) + capacity_providers = var.capacity_providers != null ? jsonencode(aws_ecs_capacity_provider.this) : "" + } } resource "aws_ecs_cluster_capacity_providers" "this" { @@ -124,11 +134,8 @@ resource "aws_ecs_cluster_capacity_providers" "this" { region = var.region - cluster_name = aws_ecs_cluster.this[0].name - capacity_providers = distinct(concat( - [for k, v in var.default_capacity_provider_strategy : try(coalesce(v.name, k))], - local.auto_scaling_group_providers - )) + cluster_name = aws_ecs_cluster.this[0].name + capacity_providers = distinct(concat(var.cluster_capacity_providers, compact(split(",", time_sleep.this[0].triggers["capacity_provider_names"])))) # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cluster-capacity-providers.html#capacity-providers-considerations dynamic "default_capacity_provider_strategy" { @@ -137,13 +144,9 @@ resource "aws_ecs_cluster_capacity_providers" "this" { content { base = default_capacity_provider_strategy.value.base capacity_provider = try(coalesce(default_capacity_provider_strategy.value.name, default_capacity_provider_strategy.key)) - weight = default_capacity_provider_strategy.value.weight + weight = coalesce(default_capacity_provider_strategy.value.weight, 1) } } - - depends_on = [ - aws_ecs_capacity_provider.this - ] } ################################################################################ @@ -151,34 +154,11 @@ resource "aws_ecs_cluster_capacity_providers" "this" { ################################################################################ locals { - managed_instances_enabled = anytrue([for k, v in local.capacity_providers : v.managed_instances_provider != null]) - - # TODO - embed the `autoscaling_capacity_providers` into a shape acceptable for - # `var.capacity_providers` so that it can be merged with the new `capacity_providers` - # for backward compatibility. Remove `autoscaling_capacity_providers` in the next major version. - capacity_providers = var.autoscaling_capacity_providers != null ? merge({ - for k, v in var.autoscaling_capacity_providers : k => { - auto_scaling_group_provider = { - autoscaling_group_arn = v.autoscaling_group_arn - managed_draining = try(v.managed_draining, null) - managed_scaling = try(v.managed_scaling, null) != null ? { - instance_warmup_period = try(v.managed_scaling.instance_warmup_period, null) - maximum_scaling_step_size = try(v.managed_scaling.maximum_scaling_step_size, null) - minimum_scaling_step_size = try(v.managed_scaling.minimum_scaling_step_size, null) - status = try(v.managed_scaling.status, null) - target_capacity = try(v.managed_scaling.target_capacity, null) - } : null - managed_termination_protection = try(v.managed_termination_protection, null) - } - managed_instances_provider = null - name = try(v.name, k) - tags = try(v.tags, {}) - } - }, var.capacity_providers != null ? var.capacity_providers : {}) : var.capacity_providers + managed_instances_enabled = anytrue([for k, v in var.capacity_providers : v.managed_instances_provider != null]) } resource "aws_ecs_capacity_provider" "this" { - for_each = var.create && local.capacity_providers != null ? local.capacity_providers : {} + for_each = var.create && var.capacity_providers != null ? var.capacity_providers : {} region = var.region @@ -349,8 +329,7 @@ resource "aws_ecs_capacity_provider" "this" { } } - # cluster = each.value.managed_instances_provider != null ? aws_ecs_cluster.this[0].id : null - cluster = aws_ecs_cluster.this[0].name + cluster = each.value.managed_instances_provider != null ? aws_ecs_cluster.this[0].name : null name = try(coalesce(each.value.name, each.key), "") @@ -358,6 +337,15 @@ resource "aws_ecs_capacity_provider" "this" { var.tags, each.value.tags, ) + + # What an awful friggin service API they created with managed instances + depends_on = [ + aws_iam_role_policy_attachment.task_exec, + aws_iam_role_policy_attachment.infrastructure, + aws_iam_role_policy_attachment.node, + aws_vpc_security_group_ingress_rule.this, + aws_vpc_security_group_egress_rule.this, + ] } ################################################################################ @@ -524,7 +512,7 @@ locals { infrastructure_iam_role_name = coalesce(var.infrastructure_iam_role_name, "${var.name}-infra", "NotProvided") } -data "aws_iam_policy_document" "infrastructure" { +data "aws_iam_policy_document" "infrastructure_assume" { count = local.create_infrastructure_iam_role ? 1 : 0 statement { @@ -549,19 +537,228 @@ resource "aws_iam_role" "infrastructure" { path = var.infrastructure_iam_role_path description = coalesce(var.infrastructure_iam_role_description, "Amazon ECS infrastructure IAM role that is used to manage your infrastructure (managed instances)") - assume_role_policy = data.aws_iam_policy_document.infrastructure[0].json + assume_role_policy = data.aws_iam_policy_document.infrastructure_assume[0].json permissions_boundary = var.infrastructure_iam_role_permissions_boundary force_detach_policies = true tags = merge(var.tags, var.infrastructure_iam_role_tags) } -# https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonECSInfrastructureRolePolicyForManagedInstances.html -resource "aws_iam_role_policy_attachment" "infrastructure_managed_instances" { +################################################################################ +# Infrastructure IAM role policy +# +# The managed policy requires role names to start with `ecsInstanceRole` +# So we are duplicating the policy here to avoid that unfortunate and surprising requirement +# +# Ref: https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonECSInfrastructureRolePolicyForManagedInstances.html +################################################################################ + +data "aws_iam_policy_document" "infrastructure" { + count = local.create_infrastructure_iam_role ? 1 : 0 + + source_policy_documents = var.infrastructure_iam_role_source_policy_documents + override_policy_documents = var.infrastructure_iam_role_override_policy_documents + + statement { + sid = "CreateLaunchTemplateForManagedInstances" + actions = ["ec2:CreateLaunchTemplate"] + resources = ["arn:${local.partition}:ec2:${local.region}:${local.account_id}:launch-template/*"] + + condition { + test = "StringEquals" + variable = "aws:RequestTag/AmazonECSManaged" + values = [true] + } + } + + statement { + sid = "CreateLaunchTemplateVersionsForManagedInstances" + actions = [ + "ec2:CreateLaunchTemplateVersion", + "ec2:ModifyLaunchTemplate", + ] + resources = ["arn:${local.partition}:ec2:${local.region}:${local.account_id}:launch-template/*"] + + condition { + test = "StringEquals" + variable = "ec2:ManagedResourceOperator" + values = ["ecs.amazonaws.com"] + } + } + + statement { + sid = "ProvisionEC2InstancesForManagedInstances" + actions = ["ec2:CreateFleet"] + resources = [ + "arn:${local.partition}:ec2:${local.region}:*:fleet/*", + "arn:${local.partition}:ec2:${local.region}:*:instance/*", + "arn:${local.partition}:ec2:${local.region}:*:network-interface/*", + "arn:${local.partition}:ec2:${local.region}:*:launch-template/*", + "arn:${local.partition}:ec2:${local.region}:*:security-group/*", + "arn:${local.partition}:ec2:${local.region}:*:subnet/*", + "arn:${local.partition}:ec2:${local.region}:*:volume/*", + "arn:${local.partition}:ec2:${local.region}:*:image/*", + ] + + condition { + test = "StringEquals" + variable = "aws:RequestTag/AmazonECSManaged" + values = [true] + } + } + + statement { + sid = "RunInstancesForManagedInstances" + actions = ["ec2:RunInstances"] + resources = [ + "arn:${local.partition}:ec2:${local.region}:*:instance/*", + "arn:${local.partition}:ec2:${local.region}:*:volume/*", + "arn:${local.partition}:ec2:${local.region}:*:network-interface/*", + ] + + condition { + test = "StringEquals" + variable = "aws:RequestTag/AmazonECSManaged" + values = [true] + } + } + + statement { + sid = "RunInstancesForECSManagedLaunchTemplates" + actions = ["ec2:RunInstances"] + resources = ["arn:${local.partition}:ec2:${local.region}:*:launch-template/*"] + + condition { + test = "StringEquals" + variable = "ec2:ResourceTag/AmazonECSManaged" + values = [true] + } + } + + statement { + sid = "RunInstancesForSupportingResources" + actions = ["ec2:RunInstances"] + resources = [ + "arn:${local.partition}:ec2:${local.region}:*:subnet/*", + "arn:${local.partition}:ec2:${local.region}:*:security-group/*", + "arn:${local.partition}:ec2:${local.region}:*:image/*", + ] + } + + statement { + sid = "TagOnCreateEC2ResourcesForManagedInstances" + actions = ["ec2:CreateTags"] + resources = [ + "arn:${local.partition}:ec2:${local.region}:*:fleet/*", + "arn:${local.partition}:ec2:${local.region}:*:launch-template/*", + "arn:${local.partition}:ec2:${local.region}:*:network-interface/*", + "arn:${local.partition}:ec2:${local.region}:*:instance/*", + "arn:${local.partition}:ec2:${local.region}:*:volume/*", + ] + + condition { + test = "StringEquals" + variable = "ec2:CreateAction" + values = [ + "CreateFleet", + "CreateLaunchTemplate", + "RunInstances", + ] + } + } + + statement { + sid = "PassInstanceRoleForManagedInstances" + actions = ["iam:PassRole"] + resources = [aws_iam_role.node[0].arn] + + condition { + test = "StringEquals" + variable = "iam:PassedToService" + values = ["ec2.amazonaws.com"] + } + } + + statement { + sid = "CreateServiceLinkedRoleForEC2Spot" + actions = ["iam:CreateServiceLinkedRole"] + resources = ["arn:${local.partition}:iam::${local.account_id}:role/aws-service-role/spot.amazonaws.com/AWSServiceRoleForEC2Spot"] + } + + statement { + sid = "DescribeEC2ResourcesManagedByECS" + actions = [ + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypes", + "ec2:DescribeLaunchTemplates", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeInstanceTypeOfferings", + "ec2:DescribeAvailabilityZones", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeVpcs", + ] + resources = ["*"] + } + + dynamic "statement" { + for_each = var.infrastructure_iam_role_statements != null ? var.infrastructure_iam_role_statements : {} + + content { + sid = try(coalesce(statement.value.sid, statement.key)) + actions = statement.value.actions + not_actions = statement.value.not_actions + effect = statement.value.effect + resources = statement.value.resources + not_resources = statement.value.not_resources + + dynamic "principals" { + for_each = statement.value.principals != null ? statement.value.principals : [] + + content { + type = principals.value.type + identifiers = principals.value.identifiers + } + } + + dynamic "not_principals" { + for_each = statement.value.not_principals != null ? statement.value.not_principals : [] + + content { + type = not_principals.value.type + identifiers = not_principals.value.identifiers + } + } + + dynamic "condition" { + for_each = statement.value.condition != null ? statement.value.condition : [] + + content { + test = condition.value.test + values = condition.value.values + variable = condition.value.variable + } + } + } + } +} + +resource "aws_iam_policy" "infrastructure" { + count = local.create_infrastructure_iam_role ? 1 : 0 + + name = var.infrastructure_iam_role_use_name_prefix ? null : local.infrastructure_iam_role_name + name_prefix = var.infrastructure_iam_role_use_name_prefix ? "${local.infrastructure_iam_role_name}-" : null + description = coalesce(var.infrastructure_iam_role_description, "ECS Managed Instances infrastructure role permissions") + policy = data.aws_iam_policy_document.infrastructure[0].json + + tags = merge(var.tags, var.infrastructure_iam_role_tags) +} + +resource "aws_iam_role_policy_attachment" "infrastructure" { count = local.create_infrastructure_iam_role ? 1 : 0 + policy_arn = aws_iam_policy.infrastructure[0].arn role = aws_iam_role.infrastructure[0].name - policy_arn = "arn:${local.partition}:iam::aws:policy/AmazonECSInfrastructureRolePolicyForManagedInstances" } ################################################################################ @@ -756,7 +953,7 @@ resource "aws_iam_instance_profile" "this" { locals { create_security_group = var.create && var.create_security_group && local.managed_instances_enabled - security_group_name = coalesce(var.security_group_name, var.name, "NotProvided") + security_group_name = coalesce(var.security_group_name, "${var.name}-cluster", "NotProvided") } resource "aws_security_group" "this" { @@ -766,7 +963,7 @@ resource "aws_security_group" "this" { name = var.security_group_use_name_prefix ? null : local.security_group_name name_prefix = var.security_group_use_name_prefix ? "${local.security_group_name}-" : null - description = var.security_group_description + description = coalesce(var.security_group_description, "Security group for ECS managed instances in cluster ${aws_ecs_cluster.this[0].name}") vpc_id = var.vpc_id tags = merge( diff --git a/modules/cluster/outputs.tf b/modules/cluster/outputs.tf index 2c5ebfb2..5a9e505a 100644 --- a/modules/cluster/outputs.tf +++ b/modules/cluster/outputs.tf @@ -44,14 +44,9 @@ output "cluster_capacity_providers" { # Capacity Provider - Autoscaling Group(s) ################################################################################ -output "autoscaling_capacity_providers" { - description = "[DEPRECATED - use `capacity_providers`] Map of autoscaling capacity providers created and their attributes" - value = aws_ecs_capacity_provider.this -} - output "capacity_providers" { description = "Map of autoscaling capacity providers created and their attributes" - value = aws_ecs_capacity_provider.this + value = var.capacity_providers != null ? jsondecode(time_sleep.this[0].triggers["capacity_providers"]) : {} } ################################################################################ diff --git a/modules/cluster/variables.tf b/modules/cluster/variables.tf index 817ea13d..da84d156 100644 --- a/modules/cluster/variables.tf +++ b/modules/cluster/variables.tf @@ -121,27 +121,20 @@ variable "cloudwatch_log_group_tags" { # Capacity Providers ################################################################################ -variable "autoscaling_capacity_providers" { - description = "[DEPRECATED - use `capacity_providers` instead] Map of autoscaling capacity provider definitions to create for the cluster" - type = map(object({ - auto_scaling_group_arn = string - managed_draining = optional(string, "ENABLED") - managed_scaling = optional(object({ - instance_warmup_period = optional(number) - maximum_scaling_step_size = optional(number) - minimum_scaling_step_size = optional(number) - status = optional(string) - target_capacity = optional(number) - })) - managed_termination_protection = optional(string) - name = optional(string) # Will fall back to use map key if not set - tags = optional(map(string), {}) - })) - default = null +variable "cluster_capacity_providers_wait_duration" { + description = "Duration to wait after the ECS cluster has become active before attaching the cluster capacity providers" + type = string + default = "20s" +} + +variable "cluster_capacity_providers" { + description = "List of capacity provider names to associate with the ECS cluster. Note: any capacity providers created by this module will be automatically added" + type = list(string) + default = [] } variable "capacity_providers" { - description = "Map of capacity provider definitions to create for the cluster" + description = "Map of capacity provider definitions to create" type = map(object({ auto_scaling_group_provider = optional(object({ auto_scaling_group_arn = string @@ -386,6 +379,48 @@ variable "infrastructure_iam_role_tags" { nullable = false } +################################################################################ +# Infrastructure IAM role policy +################################################################################ + +variable "infrastructure_iam_role_source_policy_documents" { + description = "List of IAM policy documents that are merged together into the exported document. Statements must have unique `sid`s" + type = list(string) + default = [] +} + +variable "infrastructure_iam_role_override_policy_documents" { + description = "List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid`" + type = list(string) + default = [] +} + +variable "infrastructure_iam_role_statements" { + description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" + type = map(object({ + sid = optional(string) + actions = optional(list(string)) + not_actions = optional(list(string)) + effect = optional(string, "Allow") + resources = optional(list(string)) + not_resources = optional(list(string)) + principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + not_principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + condition = optional(list(object({ + test = string + variable = string + values = list(string) + }))) + })) + default = null +} + ################################################################################ # Node IAM role & instance profile ################################################################################ @@ -555,7 +590,18 @@ variable "security_group_egress_rules" { tags = optional(map(string), {}) to_port = optional(string) })) - default = {} + default = { + all_ipv4 = { + cidr_ipv4 = "0.0.0.0/0" + description = "Allow all IPv4 traffic" + ip_protocol = "-1" + } + all_ipv6 = { + cidr_ipv6 = "::/0" + description = "Allow all IPv6 traffic" + ip_protocol = "-1" + } + } nullable = false } diff --git a/modules/cluster/versions.tf b/modules/cluster/versions.tf index 70f23a44..bceb79a7 100644 --- a/modules/cluster/versions.tf +++ b/modules/cluster/versions.tf @@ -4,7 +4,11 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.21" + version = ">= 6.23" + } + time = { + source = "hashicorp/time" + version = ">= 0.13" } } } diff --git a/modules/container-definition/README.md b/modules/container-definition/README.md index 2eab9fec..35e7a655 100644 --- a/modules/container-definition/README.md +++ b/modules/container-definition/README.md @@ -116,13 +116,13 @@ module "example_ecs_container_definition" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.5.7 | -| [aws](#requirement\_aws) | >= 6.21 | +| [aws](#requirement\_aws) | >= 6.23 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 6.21 | +| [aws](#provider\_aws) | >= 6.23 | ## Modules diff --git a/modules/container-definition/versions.tf b/modules/container-definition/versions.tf index 70f23a44..7e5b918f 100644 --- a/modules/container-definition/versions.tf +++ b/modules/container-definition/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.21" + version = ">= 6.23" } } } diff --git a/modules/service/README.md b/modules/service/README.md index 75203644..aaa4051b 100644 --- a/modules/service/README.md +++ b/modules/service/README.md @@ -170,13 +170,13 @@ module "ecs_service" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.5.7 | -| [aws](#requirement\_aws) | >= 6.21 | +| [aws](#requirement\_aws) | >= 6.23 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 6.21 | +| [aws](#provider\_aws) | >= 6.23 | ## Modules diff --git a/modules/service/main.tf b/modules/service/main.tf index b97f7042..b52afb42 100644 --- a/modules/service/main.tf +++ b/modules/service/main.tf @@ -60,7 +60,7 @@ resource "aws_ecs_service" "this" { content { base = capacity_provider_strategy.value.base capacity_provider = capacity_provider_strategy.value.capacity_provider - weight = capacity_provider_strategy.value.weight + weight = coalesce(capacity_provider_strategy.value.weight, 1) } } @@ -364,6 +364,8 @@ resource "aws_ecs_service" "this" { depends_on = [ aws_iam_role_policy_attachment.service, aws_iam_role_policy_attachment.infrastructure_iam_role_ebs_policy, + aws_vpc_security_group_ingress_rule.this, + aws_vpc_security_group_egress_rule.this, ] lifecycle { @@ -401,7 +403,7 @@ resource "aws_ecs_service" "ignore_task_definition" { content { base = capacity_provider_strategy.value.base capacity_provider = capacity_provider_strategy.value.capacity_provider - weight = capacity_provider_strategy.value.weight + weight = coalesce(capacity_provider_strategy.value.weight, 1) } } @@ -703,6 +705,8 @@ resource "aws_ecs_service" "ignore_task_definition" { depends_on = [ aws_iam_role_policy_attachment.service, aws_iam_role_policy_attachment.infrastructure_iam_role_ebs_policy, + aws_vpc_security_group_ingress_rule.this, + aws_vpc_security_group_egress_rule.this, ] lifecycle { @@ -1393,7 +1397,7 @@ resource "aws_ecs_task_set" "this" { content { base = capacity_provider_strategy.value.base capacity_provider = capacity_provider_strategy.value.capacity_provider - weight = capacity_provider_strategy.value.weight + weight = coalesce(capacity_provider_strategy.value.weight, 1) } } @@ -1476,7 +1480,7 @@ resource "aws_ecs_task_set" "ignore_task_definition" { content { base = capacity_provider_strategy.value.base capacity_provider = capacity_provider_strategy.value.capacity_provider - weight = capacity_provider_strategy.value.weight + weight = coalesce(capacity_provider_strategy.value.weight, 1) } } @@ -1864,7 +1868,7 @@ resource "aws_appautoscaling_scheduled_action" "this" { locals { create_security_group = var.create && var.create_security_group && var.network_mode == "awsvpc" - security_group_name = coalesce(var.security_group_name, var.name, "NotProvided") + security_group_name = coalesce(var.security_group_name, "${var.name}-service", "NotProvided") } data "aws_subnet" "this" { @@ -1882,7 +1886,7 @@ resource "aws_security_group" "this" { name = var.security_group_use_name_prefix ? null : local.security_group_name name_prefix = var.security_group_use_name_prefix ? "${local.security_group_name}-" : null - description = var.security_group_description + description = coalesce(var.security_group_description, "Security group for ECS Service ${var.name}") vpc_id = var.vpc_id != null ? var.vpc_id : data.aws_subnet.this[0].vpc_id tags = merge( @@ -1907,7 +1911,7 @@ resource "aws_vpc_security_group_ingress_rule" "this" { from_port = each.value.from_port ip_protocol = each.value.ip_protocol prefix_list_id = each.value.prefix_list_id - referenced_security_group_id = each.value.referenced_security_group_id + referenced_security_group_id = each.value.referenced_security_group_id == "self" ? aws_security_group.this[0].id : each.value.referenced_security_group_id security_group_id = aws_security_group.this[0].id tags = merge( var.tags, @@ -1929,7 +1933,7 @@ resource "aws_vpc_security_group_egress_rule" "this" { from_port = try(coalesce(each.value.from_port, each.value.to_port), null) ip_protocol = each.value.ip_protocol prefix_list_id = each.value.prefix_list_id - referenced_security_group_id = each.value.referenced_security_group_id + referenced_security_group_id = each.value.referenced_security_group_id == "self" ? aws_security_group.this[0].id : each.value.referenced_security_group_id security_group_id = aws_security_group.this[0].id tags = merge( var.tags, diff --git a/modules/service/versions.tf b/modules/service/versions.tf index 70f23a44..7e5b918f 100644 --- a/modules/service/versions.tf +++ b/modules/service/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.21" + version = ">= 6.23" } } } diff --git a/outputs.tf b/outputs.tf index 37a9d605..ecb6f90d 100644 --- a/outputs.tf +++ b/outputs.tf @@ -32,11 +32,6 @@ output "cluster_capacity_providers" { value = module.cluster.cluster_capacity_providers } -output "autoscaling_capacity_providers" { - description = "[DEPRECATED - use `capacity_providers`] Map of autoscaling capacity providers created and their attributes" - value = module.cluster.autoscaling_capacity_providers -} - output "capacity_providers" { description = "Map of autoscaling capacity providers created and their attributes" value = module.cluster.capacity_providers diff --git a/variables.tf b/variables.tf index 57478167..47e4a8b5 100644 --- a/variables.tf +++ b/variables.tf @@ -126,23 +126,10 @@ variable "cloudwatch_log_group_tags" { # Capacity Providers ################################################################################ -variable "autoscaling_capacity_providers" { - description = "[DEPRECATED - use `capacity_providers` instead] Map of autoscaling capacity provider definitions to create for the cluster" - type = map(object({ - auto_scaling_group_arn = string - managed_draining = optional(string, "ENABLED") - managed_scaling = optional(object({ - instance_warmup_period = optional(number) - maximum_scaling_step_size = optional(number) - minimum_scaling_step_size = optional(number) - status = optional(string) - target_capacity = optional(number) - })) - managed_termination_protection = optional(string) - name = optional(string) # Will fall back to use map key if not set - tags = optional(map(string), {}) - })) - default = null +variable "cluster_capacity_providers" { + description = "List of capacity provider names to associate with the ECS cluster. Note: any capacity providers created by this module will be automatically added" + type = list(string) + default = [] } variable "capacity_providers" { @@ -390,6 +377,48 @@ variable "infrastructure_iam_role_tags" { nullable = false } +################################################################################ +# Infrastructure IAM role policy +################################################################################ + +variable "infrastructure_iam_role_source_policy_documents" { + description = "List of IAM policy documents that are merged together into the exported document. Statements must have unique `sid`s" + type = list(string) + default = [] +} + +variable "infrastructure_iam_role_override_policy_documents" { + description = "List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid`" + type = list(string) + default = [] +} + +variable "infrastructure_iam_role_statements" { + description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" + type = map(object({ + sid = optional(string) + actions = optional(list(string)) + not_actions = optional(list(string)) + effect = optional(string, "Allow") + resources = optional(list(string)) + not_resources = optional(list(string)) + principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + not_principals = optional(list(object({ + type = string + identifiers = list(string) + }))) + condition = optional(list(object({ + test = string + variable = string + values = list(string) + }))) + })) + default = null +} + ################################################################################ # Node IAM role & instance profile ################################################################################ @@ -489,6 +518,98 @@ variable "node_iam_role_statements" { default = null } +################################################################################ +# Security Group +################################################################################ + +variable "create_security_group" { + description = "Determines if a security group is created" + type = bool + default = true + nullable = false +} + +variable "vpc_id" { + description = "The ID of the VPC where the security group will be created" + type = string + default = null +} + +variable "security_group_name" { + description = "Name to use on security group created" + type = string + default = null +} + +variable "security_group_use_name_prefix" { + description = "Determines whether the security group name (`security_group_name`) is used as a prefix" + type = bool + default = true + nullable = false +} + +variable "security_group_description" { + description = "Description of the security group created" + type = string + default = null +} + +variable "security_group_ingress_rules" { + description = "Security group ingress rules to add to the security group created" + type = map(object({ + name = optional(string) + + cidr_ipv4 = optional(string) + cidr_ipv6 = optional(string) + description = optional(string) + from_port = optional(string) + ip_protocol = optional(string, "tcp") + prefix_list_id = optional(string) + referenced_security_group_id = optional(string) + tags = optional(map(string), {}) + to_port = optional(string) + })) + default = {} + nullable = false +} + +variable "security_group_egress_rules" { + description = "Security group egress rules to add to the security group created" + type = map(object({ + name = optional(string) + + cidr_ipv4 = optional(string) + cidr_ipv6 = optional(string) + description = optional(string) + from_port = optional(string) + ip_protocol = optional(string, "tcp") + prefix_list_id = optional(string) + referenced_security_group_id = optional(string) + tags = optional(map(string), {}) + to_port = optional(string) + })) + default = { + all_ipv4 = { + cidr_ipv4 = "0.0.0.0/0" + description = "Allow all IPv4 traffic" + ip_protocol = "-1" + } + all_ipv6 = { + cidr_ipv6 = "::/0" + description = "Allow all IPv6 traffic" + ip_protocol = "-1" + } + } + nullable = false +} + +variable "security_group_tags" { + description = "A map of additional tags to add to the security group created" + type = map(string) + default = {} + nullable = false +} + ################################################################################ # Service(s) ################################################################################ diff --git a/versions.tf b/versions.tf index 70f23a44..7e5b918f 100644 --- a/versions.tf +++ b/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.21" + version = ">= 6.23" } } } diff --git a/wrappers/cluster/main.tf b/wrappers/cluster/main.tf index ce6bfae9..b63075d4 100644 --- a/wrappers/cluster/main.tf +++ b/wrappers/cluster/main.tf @@ -3,13 +3,14 @@ module "wrapper" { for_each = var.items - autoscaling_capacity_providers = try(each.value.autoscaling_capacity_providers, var.defaults.autoscaling_capacity_providers, null) - capacity_providers = try(each.value.capacity_providers, var.defaults.capacity_providers, null) - cloudwatch_log_group_class = try(each.value.cloudwatch_log_group_class, var.defaults.cloudwatch_log_group_class, null) - cloudwatch_log_group_kms_key_id = try(each.value.cloudwatch_log_group_kms_key_id, var.defaults.cloudwatch_log_group_kms_key_id, null) - cloudwatch_log_group_name = try(each.value.cloudwatch_log_group_name, var.defaults.cloudwatch_log_group_name, null) - cloudwatch_log_group_retention_in_days = try(each.value.cloudwatch_log_group_retention_in_days, var.defaults.cloudwatch_log_group_retention_in_days, 90) - cloudwatch_log_group_tags = try(each.value.cloudwatch_log_group_tags, var.defaults.cloudwatch_log_group_tags, {}) + capacity_providers = try(each.value.capacity_providers, var.defaults.capacity_providers, null) + cloudwatch_log_group_class = try(each.value.cloudwatch_log_group_class, var.defaults.cloudwatch_log_group_class, null) + cloudwatch_log_group_kms_key_id = try(each.value.cloudwatch_log_group_kms_key_id, var.defaults.cloudwatch_log_group_kms_key_id, null) + cloudwatch_log_group_name = try(each.value.cloudwatch_log_group_name, var.defaults.cloudwatch_log_group_name, null) + cloudwatch_log_group_retention_in_days = try(each.value.cloudwatch_log_group_retention_in_days, var.defaults.cloudwatch_log_group_retention_in_days, 90) + cloudwatch_log_group_tags = try(each.value.cloudwatch_log_group_tags, var.defaults.cloudwatch_log_group_tags, {}) + cluster_capacity_providers = try(each.value.cluster_capacity_providers, var.defaults.cluster_capacity_providers, []) + cluster_capacity_providers_wait_duration = try(each.value.cluster_capacity_providers_wait_duration, var.defaults.cluster_capacity_providers_wait_duration, "20s") configuration = try(each.value.configuration, var.defaults.configuration, { execute_command_configuration = { log_configuration = { @@ -17,39 +18,53 @@ module "wrapper" { } } }) - create = try(each.value.create, var.defaults.create, true) - create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.defaults.create_cloudwatch_log_group, true) - create_infrastructure_iam_role = try(each.value.create_infrastructure_iam_role, var.defaults.create_infrastructure_iam_role, true) - create_node_iam_instance_profile = try(each.value.create_node_iam_instance_profile, var.defaults.create_node_iam_instance_profile, true) - create_security_group = try(each.value.create_security_group, var.defaults.create_security_group, true) - create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, false) - create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) - default_capacity_provider_strategy = try(each.value.default_capacity_provider_strategy, var.defaults.default_capacity_provider_strategy, {}) - infrastructure_iam_role_description = try(each.value.infrastructure_iam_role_description, var.defaults.infrastructure_iam_role_description, null) - infrastructure_iam_role_name = try(each.value.infrastructure_iam_role_name, var.defaults.infrastructure_iam_role_name, null) - infrastructure_iam_role_path = try(each.value.infrastructure_iam_role_path, var.defaults.infrastructure_iam_role_path, null) - infrastructure_iam_role_permissions_boundary = try(each.value.infrastructure_iam_role_permissions_boundary, var.defaults.infrastructure_iam_role_permissions_boundary, null) - infrastructure_iam_role_tags = try(each.value.infrastructure_iam_role_tags, var.defaults.infrastructure_iam_role_tags, {}) - infrastructure_iam_role_use_name_prefix = try(each.value.infrastructure_iam_role_use_name_prefix, var.defaults.infrastructure_iam_role_use_name_prefix, true) - name = try(each.value.name, var.defaults.name, "") - node_iam_role_additional_policies = try(each.value.node_iam_role_additional_policies, var.defaults.node_iam_role_additional_policies, {}) - node_iam_role_description = try(each.value.node_iam_role_description, var.defaults.node_iam_role_description, "ECS Managed Instances node IAM role") - node_iam_role_name = try(each.value.node_iam_role_name, var.defaults.node_iam_role_name, null) - node_iam_role_override_policy_documents = try(each.value.node_iam_role_override_policy_documents, var.defaults.node_iam_role_override_policy_documents, []) - node_iam_role_path = try(each.value.node_iam_role_path, var.defaults.node_iam_role_path, null) - node_iam_role_permissions_boundary = try(each.value.node_iam_role_permissions_boundary, var.defaults.node_iam_role_permissions_boundary, null) - node_iam_role_source_policy_documents = try(each.value.node_iam_role_source_policy_documents, var.defaults.node_iam_role_source_policy_documents, []) - node_iam_role_statements = try(each.value.node_iam_role_statements, var.defaults.node_iam_role_statements, null) - node_iam_role_tags = try(each.value.node_iam_role_tags, var.defaults.node_iam_role_tags, {}) - node_iam_role_use_name_prefix = try(each.value.node_iam_role_use_name_prefix, var.defaults.node_iam_role_use_name_prefix, true) - region = try(each.value.region, var.defaults.region, null) - security_group_description = try(each.value.security_group_description, var.defaults.security_group_description, null) - security_group_egress_rules = try(each.value.security_group_egress_rules, var.defaults.security_group_egress_rules, {}) - security_group_ingress_rules = try(each.value.security_group_ingress_rules, var.defaults.security_group_ingress_rules, {}) - security_group_name = try(each.value.security_group_name, var.defaults.security_group_name, null) - security_group_tags = try(each.value.security_group_tags, var.defaults.security_group_tags, {}) - security_group_use_name_prefix = try(each.value.security_group_use_name_prefix, var.defaults.security_group_use_name_prefix, true) - service_connect_defaults = try(each.value.service_connect_defaults, var.defaults.service_connect_defaults, null) + create = try(each.value.create, var.defaults.create, true) + create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.defaults.create_cloudwatch_log_group, true) + create_infrastructure_iam_role = try(each.value.create_infrastructure_iam_role, var.defaults.create_infrastructure_iam_role, true) + create_node_iam_instance_profile = try(each.value.create_node_iam_instance_profile, var.defaults.create_node_iam_instance_profile, true) + create_security_group = try(each.value.create_security_group, var.defaults.create_security_group, true) + create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, false) + create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) + default_capacity_provider_strategy = try(each.value.default_capacity_provider_strategy, var.defaults.default_capacity_provider_strategy, {}) + infrastructure_iam_role_description = try(each.value.infrastructure_iam_role_description, var.defaults.infrastructure_iam_role_description, null) + infrastructure_iam_role_name = try(each.value.infrastructure_iam_role_name, var.defaults.infrastructure_iam_role_name, null) + infrastructure_iam_role_override_policy_documents = try(each.value.infrastructure_iam_role_override_policy_documents, var.defaults.infrastructure_iam_role_override_policy_documents, []) + infrastructure_iam_role_path = try(each.value.infrastructure_iam_role_path, var.defaults.infrastructure_iam_role_path, null) + infrastructure_iam_role_permissions_boundary = try(each.value.infrastructure_iam_role_permissions_boundary, var.defaults.infrastructure_iam_role_permissions_boundary, null) + infrastructure_iam_role_source_policy_documents = try(each.value.infrastructure_iam_role_source_policy_documents, var.defaults.infrastructure_iam_role_source_policy_documents, []) + infrastructure_iam_role_statements = try(each.value.infrastructure_iam_role_statements, var.defaults.infrastructure_iam_role_statements, null) + infrastructure_iam_role_tags = try(each.value.infrastructure_iam_role_tags, var.defaults.infrastructure_iam_role_tags, {}) + infrastructure_iam_role_use_name_prefix = try(each.value.infrastructure_iam_role_use_name_prefix, var.defaults.infrastructure_iam_role_use_name_prefix, true) + name = try(each.value.name, var.defaults.name, "") + node_iam_role_additional_policies = try(each.value.node_iam_role_additional_policies, var.defaults.node_iam_role_additional_policies, {}) + node_iam_role_description = try(each.value.node_iam_role_description, var.defaults.node_iam_role_description, "ECS Managed Instances node IAM role") + node_iam_role_name = try(each.value.node_iam_role_name, var.defaults.node_iam_role_name, null) + node_iam_role_override_policy_documents = try(each.value.node_iam_role_override_policy_documents, var.defaults.node_iam_role_override_policy_documents, []) + node_iam_role_path = try(each.value.node_iam_role_path, var.defaults.node_iam_role_path, null) + node_iam_role_permissions_boundary = try(each.value.node_iam_role_permissions_boundary, var.defaults.node_iam_role_permissions_boundary, null) + node_iam_role_source_policy_documents = try(each.value.node_iam_role_source_policy_documents, var.defaults.node_iam_role_source_policy_documents, []) + node_iam_role_statements = try(each.value.node_iam_role_statements, var.defaults.node_iam_role_statements, null) + node_iam_role_tags = try(each.value.node_iam_role_tags, var.defaults.node_iam_role_tags, {}) + node_iam_role_use_name_prefix = try(each.value.node_iam_role_use_name_prefix, var.defaults.node_iam_role_use_name_prefix, true) + region = try(each.value.region, var.defaults.region, null) + security_group_description = try(each.value.security_group_description, var.defaults.security_group_description, null) + security_group_egress_rules = try(each.value.security_group_egress_rules, var.defaults.security_group_egress_rules, { + all_ipv4 = { + cidr_ipv4 = "0.0.0.0/0" + description = "Allow all IPv4 traffic" + ip_protocol = "-1" + } + all_ipv6 = { + cidr_ipv6 = "::/0" + description = "Allow all IPv6 traffic" + ip_protocol = "-1" + } + }) + security_group_ingress_rules = try(each.value.security_group_ingress_rules, var.defaults.security_group_ingress_rules, {}) + security_group_name = try(each.value.security_group_name, var.defaults.security_group_name, null) + security_group_tags = try(each.value.security_group_tags, var.defaults.security_group_tags, {}) + security_group_use_name_prefix = try(each.value.security_group_use_name_prefix, var.defaults.security_group_use_name_prefix, true) + service_connect_defaults = try(each.value.service_connect_defaults, var.defaults.service_connect_defaults, null) setting = try(each.value.setting, var.defaults.setting, [ { name = "containerInsights" diff --git a/wrappers/cluster/versions.tf b/wrappers/cluster/versions.tf index 70f23a44..bceb79a7 100644 --- a/wrappers/cluster/versions.tf +++ b/wrappers/cluster/versions.tf @@ -4,7 +4,11 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.21" + version = ">= 6.23" + } + time = { + source = "hashicorp/time" + version = ">= 0.13" } } } diff --git a/wrappers/container-definition/versions.tf b/wrappers/container-definition/versions.tf index 70f23a44..7e5b918f 100644 --- a/wrappers/container-definition/versions.tf +++ b/wrappers/container-definition/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.21" + version = ">= 6.23" } } } diff --git a/wrappers/main.tf b/wrappers/main.tf index eee052c7..9e171bb6 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -3,13 +3,13 @@ module "wrapper" { for_each = var.items - autoscaling_capacity_providers = try(each.value.autoscaling_capacity_providers, var.defaults.autoscaling_capacity_providers, null) capacity_providers = try(each.value.capacity_providers, var.defaults.capacity_providers, null) cloudwatch_log_group_class = try(each.value.cloudwatch_log_group_class, var.defaults.cloudwatch_log_group_class, null) cloudwatch_log_group_kms_key_id = try(each.value.cloudwatch_log_group_kms_key_id, var.defaults.cloudwatch_log_group_kms_key_id, null) cloudwatch_log_group_name = try(each.value.cloudwatch_log_group_name, var.defaults.cloudwatch_log_group_name, null) cloudwatch_log_group_retention_in_days = try(each.value.cloudwatch_log_group_retention_in_days, var.defaults.cloudwatch_log_group_retention_in_days, 90) cloudwatch_log_group_tags = try(each.value.cloudwatch_log_group_tags, var.defaults.cloudwatch_log_group_tags, {}) + cluster_capacity_providers = try(each.value.cluster_capacity_providers, var.defaults.cluster_capacity_providers, []) cluster_configuration = try(each.value.cluster_configuration, var.defaults.cluster_configuration, { execute_command_configuration = { log_configuration = { @@ -25,41 +25,63 @@ module "wrapper" { value = "enabled" } ]) - cluster_tags = try(each.value.cluster_tags, var.defaults.cluster_tags, {}) - create = try(each.value.create, var.defaults.create, true) - create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.defaults.create_cloudwatch_log_group, true) - create_infrastructure_iam_role = try(each.value.create_infrastructure_iam_role, var.defaults.create_infrastructure_iam_role, true) - create_node_iam_instance_profile = try(each.value.create_node_iam_instance_profile, var.defaults.create_node_iam_instance_profile, true) - create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, false) - create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) - default_capacity_provider_strategy = try(each.value.default_capacity_provider_strategy, var.defaults.default_capacity_provider_strategy, null) - infrastructure_iam_role_description = try(each.value.infrastructure_iam_role_description, var.defaults.infrastructure_iam_role_description, null) - infrastructure_iam_role_name = try(each.value.infrastructure_iam_role_name, var.defaults.infrastructure_iam_role_name, null) - infrastructure_iam_role_path = try(each.value.infrastructure_iam_role_path, var.defaults.infrastructure_iam_role_path, null) - infrastructure_iam_role_permissions_boundary = try(each.value.infrastructure_iam_role_permissions_boundary, var.defaults.infrastructure_iam_role_permissions_boundary, null) - infrastructure_iam_role_tags = try(each.value.infrastructure_iam_role_tags, var.defaults.infrastructure_iam_role_tags, {}) - infrastructure_iam_role_use_name_prefix = try(each.value.infrastructure_iam_role_use_name_prefix, var.defaults.infrastructure_iam_role_use_name_prefix, true) - node_iam_role_additional_policies = try(each.value.node_iam_role_additional_policies, var.defaults.node_iam_role_additional_policies, {}) - node_iam_role_description = try(each.value.node_iam_role_description, var.defaults.node_iam_role_description, "ECS Managed Instances node IAM role") - node_iam_role_name = try(each.value.node_iam_role_name, var.defaults.node_iam_role_name, null) - node_iam_role_override_policy_documents = try(each.value.node_iam_role_override_policy_documents, var.defaults.node_iam_role_override_policy_documents, []) - node_iam_role_path = try(each.value.node_iam_role_path, var.defaults.node_iam_role_path, null) - node_iam_role_permissions_boundary = try(each.value.node_iam_role_permissions_boundary, var.defaults.node_iam_role_permissions_boundary, null) - node_iam_role_source_policy_documents = try(each.value.node_iam_role_source_policy_documents, var.defaults.node_iam_role_source_policy_documents, []) - node_iam_role_statements = try(each.value.node_iam_role_statements, var.defaults.node_iam_role_statements, null) - node_iam_role_tags = try(each.value.node_iam_role_tags, var.defaults.node_iam_role_tags, {}) - node_iam_role_use_name_prefix = try(each.value.node_iam_role_use_name_prefix, var.defaults.node_iam_role_use_name_prefix, true) - region = try(each.value.region, var.defaults.region, null) - services = try(each.value.services, var.defaults.services, null) - tags = try(each.value.tags, var.defaults.tags, {}) - task_exec_iam_role_description = try(each.value.task_exec_iam_role_description, var.defaults.task_exec_iam_role_description, null) - task_exec_iam_role_name = try(each.value.task_exec_iam_role_name, var.defaults.task_exec_iam_role_name, null) - task_exec_iam_role_path = try(each.value.task_exec_iam_role_path, var.defaults.task_exec_iam_role_path, null) - task_exec_iam_role_permissions_boundary = try(each.value.task_exec_iam_role_permissions_boundary, var.defaults.task_exec_iam_role_permissions_boundary, null) - task_exec_iam_role_policies = try(each.value.task_exec_iam_role_policies, var.defaults.task_exec_iam_role_policies, {}) - task_exec_iam_role_tags = try(each.value.task_exec_iam_role_tags, var.defaults.task_exec_iam_role_tags, {}) - task_exec_iam_role_use_name_prefix = try(each.value.task_exec_iam_role_use_name_prefix, var.defaults.task_exec_iam_role_use_name_prefix, true) - task_exec_iam_statements = try(each.value.task_exec_iam_statements, var.defaults.task_exec_iam_statements, null) - task_exec_secret_arns = try(each.value.task_exec_secret_arns, var.defaults.task_exec_secret_arns, []) - task_exec_ssm_param_arns = try(each.value.task_exec_ssm_param_arns, var.defaults.task_exec_ssm_param_arns, []) + cluster_tags = try(each.value.cluster_tags, var.defaults.cluster_tags, {}) + create = try(each.value.create, var.defaults.create, true) + create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.defaults.create_cloudwatch_log_group, true) + create_infrastructure_iam_role = try(each.value.create_infrastructure_iam_role, var.defaults.create_infrastructure_iam_role, true) + create_node_iam_instance_profile = try(each.value.create_node_iam_instance_profile, var.defaults.create_node_iam_instance_profile, true) + create_security_group = try(each.value.create_security_group, var.defaults.create_security_group, true) + create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, false) + create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) + default_capacity_provider_strategy = try(each.value.default_capacity_provider_strategy, var.defaults.default_capacity_provider_strategy, null) + infrastructure_iam_role_description = try(each.value.infrastructure_iam_role_description, var.defaults.infrastructure_iam_role_description, null) + infrastructure_iam_role_name = try(each.value.infrastructure_iam_role_name, var.defaults.infrastructure_iam_role_name, null) + infrastructure_iam_role_override_policy_documents = try(each.value.infrastructure_iam_role_override_policy_documents, var.defaults.infrastructure_iam_role_override_policy_documents, []) + infrastructure_iam_role_path = try(each.value.infrastructure_iam_role_path, var.defaults.infrastructure_iam_role_path, null) + infrastructure_iam_role_permissions_boundary = try(each.value.infrastructure_iam_role_permissions_boundary, var.defaults.infrastructure_iam_role_permissions_boundary, null) + infrastructure_iam_role_source_policy_documents = try(each.value.infrastructure_iam_role_source_policy_documents, var.defaults.infrastructure_iam_role_source_policy_documents, []) + infrastructure_iam_role_statements = try(each.value.infrastructure_iam_role_statements, var.defaults.infrastructure_iam_role_statements, null) + infrastructure_iam_role_tags = try(each.value.infrastructure_iam_role_tags, var.defaults.infrastructure_iam_role_tags, {}) + infrastructure_iam_role_use_name_prefix = try(each.value.infrastructure_iam_role_use_name_prefix, var.defaults.infrastructure_iam_role_use_name_prefix, true) + node_iam_role_additional_policies = try(each.value.node_iam_role_additional_policies, var.defaults.node_iam_role_additional_policies, {}) + node_iam_role_description = try(each.value.node_iam_role_description, var.defaults.node_iam_role_description, "ECS Managed Instances node IAM role") + node_iam_role_name = try(each.value.node_iam_role_name, var.defaults.node_iam_role_name, null) + node_iam_role_override_policy_documents = try(each.value.node_iam_role_override_policy_documents, var.defaults.node_iam_role_override_policy_documents, []) + node_iam_role_path = try(each.value.node_iam_role_path, var.defaults.node_iam_role_path, null) + node_iam_role_permissions_boundary = try(each.value.node_iam_role_permissions_boundary, var.defaults.node_iam_role_permissions_boundary, null) + node_iam_role_source_policy_documents = try(each.value.node_iam_role_source_policy_documents, var.defaults.node_iam_role_source_policy_documents, []) + node_iam_role_statements = try(each.value.node_iam_role_statements, var.defaults.node_iam_role_statements, null) + node_iam_role_tags = try(each.value.node_iam_role_tags, var.defaults.node_iam_role_tags, {}) + node_iam_role_use_name_prefix = try(each.value.node_iam_role_use_name_prefix, var.defaults.node_iam_role_use_name_prefix, true) + region = try(each.value.region, var.defaults.region, null) + security_group_description = try(each.value.security_group_description, var.defaults.security_group_description, null) + security_group_egress_rules = try(each.value.security_group_egress_rules, var.defaults.security_group_egress_rules, { + all_ipv4 = { + cidr_ipv4 = "0.0.0.0/0" + description = "Allow all IPv4 traffic" + ip_protocol = "-1" + } + all_ipv6 = { + cidr_ipv6 = "::/0" + description = "Allow all IPv6 traffic" + ip_protocol = "-1" + } + }) + security_group_ingress_rules = try(each.value.security_group_ingress_rules, var.defaults.security_group_ingress_rules, {}) + security_group_name = try(each.value.security_group_name, var.defaults.security_group_name, null) + security_group_tags = try(each.value.security_group_tags, var.defaults.security_group_tags, {}) + security_group_use_name_prefix = try(each.value.security_group_use_name_prefix, var.defaults.security_group_use_name_prefix, true) + services = try(each.value.services, var.defaults.services, null) + tags = try(each.value.tags, var.defaults.tags, {}) + task_exec_iam_role_description = try(each.value.task_exec_iam_role_description, var.defaults.task_exec_iam_role_description, null) + task_exec_iam_role_name = try(each.value.task_exec_iam_role_name, var.defaults.task_exec_iam_role_name, null) + task_exec_iam_role_path = try(each.value.task_exec_iam_role_path, var.defaults.task_exec_iam_role_path, null) + task_exec_iam_role_permissions_boundary = try(each.value.task_exec_iam_role_permissions_boundary, var.defaults.task_exec_iam_role_permissions_boundary, null) + task_exec_iam_role_policies = try(each.value.task_exec_iam_role_policies, var.defaults.task_exec_iam_role_policies, {}) + task_exec_iam_role_tags = try(each.value.task_exec_iam_role_tags, var.defaults.task_exec_iam_role_tags, {}) + task_exec_iam_role_use_name_prefix = try(each.value.task_exec_iam_role_use_name_prefix, var.defaults.task_exec_iam_role_use_name_prefix, true) + task_exec_iam_statements = try(each.value.task_exec_iam_statements, var.defaults.task_exec_iam_statements, null) + task_exec_secret_arns = try(each.value.task_exec_secret_arns, var.defaults.task_exec_secret_arns, []) + task_exec_ssm_param_arns = try(each.value.task_exec_ssm_param_arns, var.defaults.task_exec_ssm_param_arns, []) + vpc_id = try(each.value.vpc_id, var.defaults.vpc_id, null) } diff --git a/wrappers/service/versions.tf b/wrappers/service/versions.tf index 70f23a44..7e5b918f 100644 --- a/wrappers/service/versions.tf +++ b/wrappers/service/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.21" + version = ">= 6.23" } } } diff --git a/wrappers/versions.tf b/wrappers/versions.tf index 70f23a44..7e5b918f 100644 --- a/wrappers/versions.tf +++ b/wrappers/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.21" + version = ">= 6.23" } } } From c73fe47e347018cea4e2ef5d481a0876333176c0 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Tue, 16 Dec 2025 18:12:08 -0600 Subject: [PATCH 06/10] fix: Correct examples for progressive deployments --- README.md | 2 +- examples/complete/main.tf | 52 +++++++++++++++++++++++++++++--- examples/ec2-autoscaling/main.tf | 52 +++++++++++++++++++++++++++++--- modules/service/README.md | 5 +-- modules/service/main.tf | 25 ++++++++++----- modules/service/variables.tf | 6 ++-- variables.tf | 6 ++-- 7 files changed, 123 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f48a9589..bb4b8850 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,7 @@ No resources. | [security\_group\_name](#input\_security\_group\_name) | Name to use on security group created | `string` | `null` | no | | [security\_group\_tags](#input\_security\_group\_tags) | A map of additional tags to add to the security group created | `map(string)` | `{}` | no | | [security\_group\_use\_name\_prefix](#input\_security\_group\_use\_name\_prefix) | Determines whether the security group name (`security_group_name`) is used as a prefix | `bool` | `true` | no | -| [services](#input\_services) | Map of service definitions to create |
map(object({
create = optional(bool)
create_service = optional(bool)
tags = optional(map(string))

# Service
ignore_task_definition_changes = optional(bool)
alarms = optional(object({
alarm_names = list(string)
enable = optional(bool)
rollback = optional(bool)
}))
availability_zone_rebalancing = optional(string)
capacity_provider_strategy = optional(map(object({
base = optional(number)
capacity_provider = string
weight = optional(number)
})))
deployment_circuit_breaker = optional(object({
enable = bool
rollback = bool
}))
deployment_configuration = optional(object({
strategy = optional(string)
bake_time_in_minutes = optional(string)
canary_configuration = optional(object({
canary_bake_time_in_minutes = optional(string)
canary_percent = optional(string)
}))
linear_configuration = optional(object({
step_bake_time_in_minutes = optional(string)
step_percent = optional(string)
}))
lifecycle_hook = optional(map(object({
hook_target_arn = string
role_arn = string
lifecycle_stages = list(string)
hook_details = optional(string)
})))
}))
deployment_controller = optional(object({
type = optional(string)
}))
deployment_maximum_percent = optional(number, 200)
deployment_minimum_healthy_percent = optional(number, 66)
desired_count = optional(number, 1)
enable_ecs_managed_tags = optional(bool)
enable_execute_command = optional(bool)
force_delete = optional(bool)
force_new_deployment = optional(bool)
health_check_grace_period_seconds = optional(number)
launch_type = optional(string)
load_balancer = optional(map(object({
container_name = string
container_port = number
elb_name = optional(string)
target_group_arn = optional(string)
advanced_configuration = optional(object({
alternate_target_group_arn = string
production_listener_rule = string
role_arn = string
test_listener_rule = optional(string)
}))
})))
name = optional(string) # Will fall back to use map key if not set
assign_public_ip = optional(bool)
security_group_ids = optional(list(string))
subnet_ids = optional(list(string))
ordered_placement_strategy = optional(map(object({
field = optional(string)
type = string
})))
placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
platform_version = optional(string)
propagate_tags = optional(string)
scheduling_strategy = optional(string)
service_connect_configuration = optional(object({
enabled = optional(bool)
log_configuration = optional(object({
log_driver = string
options = optional(map(string))
secret_option = optional(list(object({
name = string
value_from = string
})))
}))
namespace = optional(string)
service = optional(list(object({
client_alias = optional(object({
dns_name = optional(string)
port = number
test_traffic_rules = optional(list(object({
header = optional(object({
name = string
value = object({
exact = string
})
}))
})))
}))
discovery_name = optional(string)
ingress_port_override = optional(number)
port_name = string
timeout = optional(object({
idle_timeout_seconds = optional(number)
per_request_timeout_seconds = optional(number)
}))
tls = optional(object({
issuer_cert_authority = object({
aws_pca_authority_arn = string
})
kms_key = optional(string)
role_arn = optional(string)
}))
})))
}))
service_registries = optional(object({
container_name = optional(string)
container_port = optional(number)
port = optional(number)
registry_arn = string
}))
sigint_rollback = optional(bool)
timeouts = optional(object({
create = optional(string)
update = optional(string)
delete = optional(string)
}))
triggers = optional(map(string))
volume_configuration = optional(object({
name = string
managed_ebs_volume = object({
encrypted = optional(bool)
file_system_type = optional(string)
iops = optional(number)
kms_key_id = optional(string)
size_in_gb = optional(number)
snapshot_id = optional(string)
tag_specifications = optional(list(object({
propagate_tags = optional(string)
resource_type = string
tags = optional(map(string))
})))
throughput = optional(number)
volume_type = optional(string)
})
}))
vpc_lattice_configurations = optional(object({
role_arn = string
target_group_arn = string
port_name = string
}))
wait_for_steady_state = optional(bool)
service_tags = optional(map(string))
# Service - IAM Role
create_iam_role = optional(bool)
iam_role_arn = optional(string)
iam_role_name = optional(string)
iam_role_use_name_prefix = optional(bool)
iam_role_path = optional(string)
iam_role_description = optional(string)
iam_role_permissions_boundary = optional(string)
iam_role_tags = optional(map(string))
iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
# Task Definition
create_task_definition = optional(bool)
task_definition_arn = optional(string)
container_definitions = optional(map(object({
operating_system_family = optional(string)
tags = optional(map(string))

# Container definition
command = optional(list(string))
cpu = optional(number)
credentialSpecs = optional(list(string))
dependsOn = optional(list(object({
condition = string
containerName = string
})))
disableNetworking = optional(bool)
dnsSearchDomains = optional(list(string))
dnsServers = optional(list(string))
dockerLabels = optional(map(string))
dockerSecurityOptions = optional(list(string))
enable_execute_command = optional(bool)
entrypoint = optional(list(string))
environment = optional(list(object({
name = string
value = string
})))
environmentFiles = optional(list(object({
type = string
value = string
})))
essential = optional(bool)
extraHosts = optional(list(object({
hostname = string
ipAddress = string
})))
firelensConfiguration = optional(object({
options = optional(map(string))
type = optional(string)
}))
healthCheck = optional(object({
command = optional(list(string))
interval = optional(number)
retries = optional(number)
startPeriod = optional(number)
timeout = optional(number)
}))
hostname = optional(string)
image = optional(string)
interactive = optional(bool)
links = optional(list(string))
linuxParameters = optional(object({
capabilities = optional(object({
add = optional(list(string))
drop = optional(list(string))
}))
devices = optional(list(object({
containerPath = optional(string)
hostPath = optional(string)
permissions = optional(list(string))
})))
initProcessEnabled = optional(bool)
maxSwap = optional(number)
sharedMemorySize = optional(number)
swappiness = optional(number)
tmpfs = optional(list(object({
containerPath = string
mountOptions = optional(list(string))
size = number
})))
}))
logConfiguration = optional(object({
logDriver = optional(string)
options = optional(map(string))
secretOptions = optional(list(object({
name = string
valueFrom = string
})))
}))
memory = optional(number)
memoryReservation = optional(number)
mountPoints = optional(list(object({
containerPath = optional(string)
readOnly = optional(bool)
sourceVolume = optional(string)
})), [])
name = optional(string)
portMappings = optional(list(object({
appProtocol = optional(string)
containerPort = optional(number)
containerPortRange = optional(string)
hostPort = optional(number)
name = optional(string)
protocol = optional(string)
})), [])
privileged = optional(bool)
pseudoTerminal = optional(bool)
readonlyRootFilesystem = optional(bool)
repositoryCredentials = optional(object({
credentialsParameter = optional(string)
}))
resourceRequirements = optional(list(object({
type = string
value = string
})))
restartPolicy = optional(object({
enabled = optional(bool)
ignoredExitCodes = optional(list(number))
restartAttemptPeriod = optional(number)
}))
secrets = optional(list(object({
name = string
valueFrom = string
})))
startTimeout = optional(number)
stopTimeout = optional(number)
systemControls = optional(list(object({
namespace = optional(string)
value = optional(string)
})))
ulimits = optional(list(object({
hardLimit = number
name = string
softLimit = number
})))
user = optional(string)
versionConsistency = optional(string)
volumesFrom = optional(list(object({
readOnly = optional(bool)
sourceContainer = optional(string)
})))
workingDirectory = optional(string)

# Cloudwatch Log Group
service = optional(string, "")
enable_cloudwatch_logging = optional(bool)
create_cloudwatch_log_group = optional(bool)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool)
cloudwatch_log_group_class = optional(string)
cloudwatch_log_group_retention_in_days = optional(number)
cloudwatch_log_group_kms_key_id = optional(string)
})))
cpu = optional(number, 1024)
enable_fault_injection = optional(bool)
ephemeral_storage = optional(object({
size_in_gib = number
}))
family = optional(string)
ipc_mode = optional(string)
memory = optional(number, 2048)
network_mode = optional(string)
pid_mode = optional(string)
proxy_configuration = optional(object({
container_name = string
properties = optional(map(string))
type = optional(string)
}))
requires_compatibilities = optional(list(string))
runtime_platform = optional(object({
cpu_architecture = optional(string)
operating_system_family = optional(string)
}))
skip_destroy = optional(bool)
task_definition_placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
track_latest = optional(bool)
volume = optional(map(object({
configure_at_launch = optional(bool)
docker_volume_configuration = optional(object({
autoprovision = optional(bool)
driver = optional(string)
driver_opts = optional(map(string))
labels = optional(map(string))
scope = optional(string)
}))
efs_volume_configuration = optional(object({
authorization_config = optional(object({
access_point_id = optional(string)
iam = optional(string)
}))
file_system_id = string
root_directory = optional(string)
transit_encryption = optional(string)
transit_encryption_port = optional(number)
}))
fsx_windows_file_server_volume_configuration = optional(object({
authorization_config = optional(object({
credentials_parameter = string
domain = string
}))
file_system_id = string
root_directory = string
}))
host_path = optional(string)
name = optional(string)
})))
task_tags = optional(map(string))
# Task Execution - IAM Role
create_task_exec_iam_role = optional(bool)
task_exec_iam_role_arn = optional(string)
task_exec_iam_role_name = optional(string)
task_exec_iam_role_use_name_prefix = optional(bool)
task_exec_iam_role_path = optional(string)
task_exec_iam_role_description = optional(string)
task_exec_iam_role_permissions_boundary = optional(string)
task_exec_iam_role_tags = optional(map(string))
task_exec_iam_role_policies = optional(map(string))
task_exec_iam_role_max_session_duration = optional(number)
create_task_exec_policy = optional(bool)
task_exec_ssm_param_arns = optional(list(string))
task_exec_secret_arns = optional(list(string))
task_exec_iam_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
task_exec_iam_policy_path = optional(string)
# Tasks - IAM Role
create_tasks_iam_role = optional(bool)
tasks_iam_role_arn = optional(string)
tasks_iam_role_name = optional(string)
tasks_iam_role_use_name_prefix = optional(bool)
tasks_iam_role_path = optional(string)
tasks_iam_role_description = optional(string)
tasks_iam_role_permissions_boundary = optional(string)
tasks_iam_role_tags = optional(map(string))
tasks_iam_role_policies = optional(map(string))
tasks_iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
# Task Set
external_id = optional(string)
scale = optional(object({
unit = optional(string)
value = optional(number)
}))
wait_until_stable = optional(bool)
wait_until_stable_timeout = optional(string)
# Autoscaling
enable_autoscaling = optional(bool)
autoscaling_min_capacity = optional(number)
autoscaling_max_capacity = optional(number)
autoscaling_policies = optional(map(object({
name = optional(string) # Will fall back to the key name if not provided
policy_type = optional(string)
predictive_scaling_policy_configuration = optional(object({
max_capacity_breach_behavior = optional(string)
max_capacity_buffer = optional(number)
metric_specification = list(object({
customized_capacity_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_load_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_scaling_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
predefined_load_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_metric_pair_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_scaling_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
target_value = number
}))
mode = optional(string)
scheduling_buffer_time = optional(number)
}))
step_scaling_policy_configuration = optional(object({
adjustment_type = optional(string)
cooldown = optional(number)
metric_aggregation_type = optional(string)
min_adjustment_magnitude = optional(number)
step_adjustment = optional(list(object({
metric_interval_lower_bound = optional(string)
metric_interval_upper_bound = optional(string)
scaling_adjustment = number
})))
}))
target_tracking_scaling_policy_configuration = optional(object({
customized_metric_specification = optional(object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
metrics = optional(list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = string
namespace = string
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
})))
namespace = optional(string)
statistic = optional(string)
unit = optional(string)
}))

disable_scale_in = optional(bool)
predefined_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
scale_in_cooldown = optional(number)
scale_out_cooldown = optional(number)
target_value = optional(number)
}))
})))
autoscaling_scheduled_actions = optional(map(object({
name = optional(string)
min_capacity = number
max_capacity = number
schedule = string
start_time = optional(string)
end_time = optional(string)
timezone = optional(string)
})))
autoscaling_suspended_state = optional(object({
dynamic_scaling_in_suspended = optional(bool)
dynamic_scaling_out_suspended = optional(bool)
scheduled_scaling_suspended = optional(bool)
}))
# Security Group
create_security_group = optional(bool)
vpc_id = optional(string)
security_group_name = optional(string)
security_group_use_name_prefix = optional(bool)
security_group_description = optional(string)
security_group_ingress_rules = optional(map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string))
to_port = optional(string)
})))
security_group_egress_rules = optional(map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string))
to_port = optional(string)
})))
security_group_tags = optional(map(string))
# ECS Infrastructure IAM Role
create_infrastructure_iam_role = optional(bool)
infrastructure_iam_role_arn = optional(string)
infrastructure_iam_role_name = optional(string)
infrastructure_iam_role_use_name_prefix = optional(bool)
infrastructure_iam_role_path = optional(string)
infrastructure_iam_role_description = optional(string)
infrastructure_iam_role_permissions_boundary = optional(string)
infrastructure_iam_role_tags = optional(map(string))
}))
| `null` | no | +| [services](#input\_services) | Map of service definitions to create |
map(object({
create = optional(bool)
create_service = optional(bool)
tags = optional(map(string))

# Service
ignore_task_definition_changes = optional(bool)
alarms = optional(object({
alarm_names = list(string)
enable = optional(bool)
rollback = optional(bool)
}))
availability_zone_rebalancing = optional(string)
capacity_provider_strategy = optional(map(object({
base = optional(number)
capacity_provider = string
weight = optional(number)
})))
deployment_circuit_breaker = optional(object({
enable = bool
rollback = bool
}))
deployment_configuration = optional(object({
strategy = optional(string)
bake_time_in_minutes = optional(string)
canary_configuration = optional(object({
canary_bake_time_in_minutes = optional(string)
canary_percent = optional(string)
}))
linear_configuration = optional(object({
step_bake_time_in_minutes = optional(string)
step_percent = optional(string)
}))
lifecycle_hook = optional(map(object({
hook_target_arn = string
role_arn = optional(string)
lifecycle_stages = list(string)
hook_details = optional(string)
})))
}))
deployment_controller = optional(object({
type = optional(string)
}))
deployment_maximum_percent = optional(number, 200)
deployment_minimum_healthy_percent = optional(number, 66)
desired_count = optional(number, 1)
enable_ecs_managed_tags = optional(bool)
enable_execute_command = optional(bool)
force_delete = optional(bool)
force_new_deployment = optional(bool)
health_check_grace_period_seconds = optional(number)
launch_type = optional(string)
load_balancer = optional(map(object({
container_name = string
container_port = number
elb_name = optional(string)
target_group_arn = optional(string)
advanced_configuration = optional(object({
alternate_target_group_arn = string
production_listener_rule = string # Should be optional but bug in provider
role_arn = optional(string)
test_listener_rule = optional(string)
}))
})))
name = optional(string) # Will fall back to use map key if not set
assign_public_ip = optional(bool)
security_group_ids = optional(list(string))
subnet_ids = optional(list(string))
ordered_placement_strategy = optional(map(object({
field = optional(string)
type = string
})))
placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
platform_version = optional(string)
propagate_tags = optional(string)
scheduling_strategy = optional(string)
service_connect_configuration = optional(object({
enabled = optional(bool)
log_configuration = optional(object({
log_driver = string
options = optional(map(string))
secret_option = optional(list(object({
name = string
value_from = string
})))
}))
namespace = optional(string)
service = optional(list(object({
client_alias = optional(object({
dns_name = optional(string)
port = number
test_traffic_rules = optional(list(object({
header = optional(object({
name = string
value = object({
exact = string
})
}))
})))
}))
discovery_name = optional(string)
ingress_port_override = optional(number)
port_name = string
timeout = optional(object({
idle_timeout_seconds = optional(number)
per_request_timeout_seconds = optional(number)
}))
tls = optional(object({
issuer_cert_authority = object({
aws_pca_authority_arn = string
})
kms_key = optional(string)
role_arn = optional(string)
}))
})))
}))
service_registries = optional(object({
container_name = optional(string)
container_port = optional(number)
port = optional(number)
registry_arn = string
}))
sigint_rollback = optional(bool)
timeouts = optional(object({
create = optional(string)
update = optional(string)
delete = optional(string)
}))
triggers = optional(map(string))
volume_configuration = optional(object({
name = string
managed_ebs_volume = object({
encrypted = optional(bool)
file_system_type = optional(string)
iops = optional(number)
kms_key_id = optional(string)
size_in_gb = optional(number)
snapshot_id = optional(string)
tag_specifications = optional(list(object({
propagate_tags = optional(string)
resource_type = string
tags = optional(map(string))
})))
throughput = optional(number)
volume_type = optional(string)
})
}))
vpc_lattice_configurations = optional(object({
role_arn = string
target_group_arn = string
port_name = string
}))
wait_for_steady_state = optional(bool)
service_tags = optional(map(string))
# Service - IAM Role
create_iam_role = optional(bool)
iam_role_arn = optional(string)
iam_role_name = optional(string)
iam_role_use_name_prefix = optional(bool)
iam_role_path = optional(string)
iam_role_description = optional(string)
iam_role_permissions_boundary = optional(string)
iam_role_tags = optional(map(string))
iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
# Task Definition
create_task_definition = optional(bool)
task_definition_arn = optional(string)
container_definitions = optional(map(object({
operating_system_family = optional(string)
tags = optional(map(string))

# Container definition
command = optional(list(string))
cpu = optional(number)
credentialSpecs = optional(list(string))
dependsOn = optional(list(object({
condition = string
containerName = string
})))
disableNetworking = optional(bool)
dnsSearchDomains = optional(list(string))
dnsServers = optional(list(string))
dockerLabels = optional(map(string))
dockerSecurityOptions = optional(list(string))
enable_execute_command = optional(bool)
entrypoint = optional(list(string))
environment = optional(list(object({
name = string
value = string
})))
environmentFiles = optional(list(object({
type = string
value = string
})))
essential = optional(bool)
extraHosts = optional(list(object({
hostname = string
ipAddress = string
})))
firelensConfiguration = optional(object({
options = optional(map(string))
type = optional(string)
}))
healthCheck = optional(object({
command = optional(list(string))
interval = optional(number)
retries = optional(number)
startPeriod = optional(number)
timeout = optional(number)
}))
hostname = optional(string)
image = optional(string)
interactive = optional(bool)
links = optional(list(string))
linuxParameters = optional(object({
capabilities = optional(object({
add = optional(list(string))
drop = optional(list(string))
}))
devices = optional(list(object({
containerPath = optional(string)
hostPath = optional(string)
permissions = optional(list(string))
})))
initProcessEnabled = optional(bool)
maxSwap = optional(number)
sharedMemorySize = optional(number)
swappiness = optional(number)
tmpfs = optional(list(object({
containerPath = string
mountOptions = optional(list(string))
size = number
})))
}))
logConfiguration = optional(object({
logDriver = optional(string)
options = optional(map(string))
secretOptions = optional(list(object({
name = string
valueFrom = string
})))
}))
memory = optional(number)
memoryReservation = optional(number)
mountPoints = optional(list(object({
containerPath = optional(string)
readOnly = optional(bool)
sourceVolume = optional(string)
})), [])
name = optional(string)
portMappings = optional(list(object({
appProtocol = optional(string)
containerPort = optional(number)
containerPortRange = optional(string)
hostPort = optional(number)
name = optional(string)
protocol = optional(string)
})), [])
privileged = optional(bool)
pseudoTerminal = optional(bool)
readonlyRootFilesystem = optional(bool)
repositoryCredentials = optional(object({
credentialsParameter = optional(string)
}))
resourceRequirements = optional(list(object({
type = string
value = string
})))
restartPolicy = optional(object({
enabled = optional(bool)
ignoredExitCodes = optional(list(number))
restartAttemptPeriod = optional(number)
}))
secrets = optional(list(object({
name = string
valueFrom = string
})))
startTimeout = optional(number)
stopTimeout = optional(number)
systemControls = optional(list(object({
namespace = optional(string)
value = optional(string)
})))
ulimits = optional(list(object({
hardLimit = number
name = string
softLimit = number
})))
user = optional(string)
versionConsistency = optional(string)
volumesFrom = optional(list(object({
readOnly = optional(bool)
sourceContainer = optional(string)
})))
workingDirectory = optional(string)

# Cloudwatch Log Group
service = optional(string, "")
enable_cloudwatch_logging = optional(bool)
create_cloudwatch_log_group = optional(bool)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool)
cloudwatch_log_group_class = optional(string)
cloudwatch_log_group_retention_in_days = optional(number)
cloudwatch_log_group_kms_key_id = optional(string)
})))
cpu = optional(number, 1024)
enable_fault_injection = optional(bool)
ephemeral_storage = optional(object({
size_in_gib = number
}))
family = optional(string)
ipc_mode = optional(string)
memory = optional(number, 2048)
network_mode = optional(string)
pid_mode = optional(string)
proxy_configuration = optional(object({
container_name = string
properties = optional(map(string))
type = optional(string)
}))
requires_compatibilities = optional(list(string))
runtime_platform = optional(object({
cpu_architecture = optional(string)
operating_system_family = optional(string)
}))
skip_destroy = optional(bool)
task_definition_placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
track_latest = optional(bool)
volume = optional(map(object({
configure_at_launch = optional(bool)
docker_volume_configuration = optional(object({
autoprovision = optional(bool)
driver = optional(string)
driver_opts = optional(map(string))
labels = optional(map(string))
scope = optional(string)
}))
efs_volume_configuration = optional(object({
authorization_config = optional(object({
access_point_id = optional(string)
iam = optional(string)
}))
file_system_id = string
root_directory = optional(string)
transit_encryption = optional(string)
transit_encryption_port = optional(number)
}))
fsx_windows_file_server_volume_configuration = optional(object({
authorization_config = optional(object({
credentials_parameter = string
domain = string
}))
file_system_id = string
root_directory = string
}))
host_path = optional(string)
name = optional(string)
})))
task_tags = optional(map(string))
# Task Execution - IAM Role
create_task_exec_iam_role = optional(bool)
task_exec_iam_role_arn = optional(string)
task_exec_iam_role_name = optional(string)
task_exec_iam_role_use_name_prefix = optional(bool)
task_exec_iam_role_path = optional(string)
task_exec_iam_role_description = optional(string)
task_exec_iam_role_permissions_boundary = optional(string)
task_exec_iam_role_tags = optional(map(string))
task_exec_iam_role_policies = optional(map(string))
task_exec_iam_role_max_session_duration = optional(number)
create_task_exec_policy = optional(bool)
task_exec_ssm_param_arns = optional(list(string))
task_exec_secret_arns = optional(list(string))
task_exec_iam_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
task_exec_iam_policy_path = optional(string)
# Tasks - IAM Role
create_tasks_iam_role = optional(bool)
tasks_iam_role_arn = optional(string)
tasks_iam_role_name = optional(string)
tasks_iam_role_use_name_prefix = optional(bool)
tasks_iam_role_path = optional(string)
tasks_iam_role_description = optional(string)
tasks_iam_role_permissions_boundary = optional(string)
tasks_iam_role_tags = optional(map(string))
tasks_iam_role_policies = optional(map(string))
tasks_iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
# Task Set
external_id = optional(string)
scale = optional(object({
unit = optional(string)
value = optional(number)
}))
wait_until_stable = optional(bool)
wait_until_stable_timeout = optional(string)
# Autoscaling
enable_autoscaling = optional(bool)
autoscaling_min_capacity = optional(number)
autoscaling_max_capacity = optional(number)
autoscaling_policies = optional(map(object({
name = optional(string) # Will fall back to the key name if not provided
policy_type = optional(string)
predictive_scaling_policy_configuration = optional(object({
max_capacity_breach_behavior = optional(string)
max_capacity_buffer = optional(number)
metric_specification = list(object({
customized_capacity_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_load_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_scaling_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
predefined_load_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_metric_pair_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_scaling_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
target_value = number
}))
mode = optional(string)
scheduling_buffer_time = optional(number)
}))
step_scaling_policy_configuration = optional(object({
adjustment_type = optional(string)
cooldown = optional(number)
metric_aggregation_type = optional(string)
min_adjustment_magnitude = optional(number)
step_adjustment = optional(list(object({
metric_interval_lower_bound = optional(string)
metric_interval_upper_bound = optional(string)
scaling_adjustment = number
})))
}))
target_tracking_scaling_policy_configuration = optional(object({
customized_metric_specification = optional(object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
metrics = optional(list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = string
namespace = string
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
})))
namespace = optional(string)
statistic = optional(string)
unit = optional(string)
}))

disable_scale_in = optional(bool)
predefined_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
scale_in_cooldown = optional(number)
scale_out_cooldown = optional(number)
target_value = optional(number)
}))
})))
autoscaling_scheduled_actions = optional(map(object({
name = optional(string)
min_capacity = number
max_capacity = number
schedule = string
start_time = optional(string)
end_time = optional(string)
timezone = optional(string)
})))
autoscaling_suspended_state = optional(object({
dynamic_scaling_in_suspended = optional(bool)
dynamic_scaling_out_suspended = optional(bool)
scheduled_scaling_suspended = optional(bool)
}))
# Security Group
create_security_group = optional(bool)
vpc_id = optional(string)
security_group_name = optional(string)
security_group_use_name_prefix = optional(bool)
security_group_description = optional(string)
security_group_ingress_rules = optional(map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string))
to_port = optional(string)
})))
security_group_egress_rules = optional(map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string))
to_port = optional(string)
})))
security_group_tags = optional(map(string))
# ECS Infrastructure IAM Role
create_infrastructure_iam_role = optional(bool)
infrastructure_iam_role_arn = optional(string)
infrastructure_iam_role_name = optional(string)
infrastructure_iam_role_use_name_prefix = optional(bool)
infrastructure_iam_role_path = optional(string)
infrastructure_iam_role_description = optional(string)
infrastructure_iam_role_permissions_boundary = optional(string)
infrastructure_iam_role_tags = optional(map(string))
}))
| `null` | no | | [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | | [task\_exec\_iam\_role\_description](#input\_task\_exec\_iam\_role\_description) | Description of the role | `string` | `null` | no | | [task\_exec\_iam\_role\_name](#input\_task\_exec\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 6e021cd8..39da4a50 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -220,9 +220,14 @@ module "ecs" { load_balancer = { service = { - target_group_arn = module.alb.target_groups["ex_ecs"].arn + target_group_arn = module.alb.target_groups["ex-ecs"].arn container_name = local.container_name container_port = local.container_port + + advanced_configuration = { + alternate_target_group_arn = module.alb.target_groups["ex-ecs-alt"].arn + production_listener_rule = module.alb.listener_rules["ex-http/ex-forward"].arn + } } } @@ -328,18 +333,57 @@ module "alb" { } listeners = { - ex_http = { + ex-http = { port = 80 protocol = "HTTP" forward = { - target_group_key = "ex_ecs" + target_group_key = "ex-ecs" + } + + rules = { + ex-forward = { + priority = 100 + actions = [{ + forward = { + target_group_key = "ex-ecs" + } + }] + conditions = [{ + path_pattern = { + values = ["/"] + } + }] + } } } } target_groups = { - ex_ecs = { + ex-ecs = { + backend_protocol = "HTTP" + backend_port = local.container_port + target_type = "ip" + deregistration_delay = 5 + load_balancing_cross_zone_enabled = true + + health_check = { + enabled = true + healthy_threshold = 5 + interval = 30 + matcher = "200" + path = "/" + port = "traffic-port" + protocol = "HTTP" + timeout = 5 + unhealthy_threshold = 2 + } + + # Theres nothing to attach here in this definition. Instead, + # ECS will attach the IPs of the tasks to this target group + create_attachment = false + } + ex-ecs-alt = { backend_protocol = "HTTP" backend_port = local.container_port target_type = "ip" diff --git a/examples/ec2-autoscaling/main.tf b/examples/ec2-autoscaling/main.tf index 0d7c2187..08791927 100644 --- a/examples/ec2-autoscaling/main.tf +++ b/examples/ec2-autoscaling/main.tf @@ -186,9 +186,14 @@ module "ecs_service" { load_balancer = { service = { - target_group_arn = module.alb.target_groups["ex_ecs"].arn + target_group_arn = module.alb.target_groups["ex-ecs"].arn container_name = local.container_name container_port = local.container_port + + advanced_configuration = { + alternate_target_group_arn = module.alb.target_groups["ex-ecs-alt"].arn + production_listener_rule = module.alb.listener_rules["ex-http/ex-forward"].arn + } } } @@ -244,18 +249,57 @@ module "alb" { } listeners = { - ex_http = { + ex-http = { port = 80 protocol = "HTTP" forward = { - target_group_key = "ex_ecs" + target_group_key = "ex-ecs" + } + + rules = { + ex-forward = { + priority = 100 + actions = [{ + forward = { + target_group_key = "ex-ecs" + } + }] + conditions = [{ + path_pattern = { + values = ["/"] + } + }] + } } } } target_groups = { - ex_ecs = { + ex-ecs = { + backend_protocol = "HTTP" + backend_port = local.container_port + target_type = "ip" + deregistration_delay = 5 + load_balancing_cross_zone_enabled = true + + health_check = { + enabled = true + healthy_threshold = 5 + interval = 30 + matcher = "200" + path = "/" + port = "traffic-port" + protocol = "HTTP" + timeout = 5 + unhealthy_threshold = 2 + } + + # Theres nothing to attach here in this definition. Instead, + # ECS will attach the IPs of the tasks to this target group + create_attachment = false + } + ex-ecs-alt = { backend_protocol = "HTTP" backend_port = local.container_port target_type = "ip" diff --git a/modules/service/README.md b/modules/service/README.md index aaa4051b..ed7730c5 100644 --- a/modules/service/README.md +++ b/modules/service/README.md @@ -204,6 +204,7 @@ module "ecs_service" { | [aws_iam_role.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.tasks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy_attachment.infrastructure_iam_role_ebs_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.infrastructure_iam_role_load_balancer_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.infrastructure_iam_role_vpc_lattice_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | @@ -251,7 +252,7 @@ module "ecs_service" { | [create\_task\_exec\_policy](#input\_create\_task\_exec\_policy) | Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters | `bool` | `true` | no | | [create\_tasks\_iam\_role](#input\_create\_tasks\_iam\_role) | Determines whether the ECS tasks IAM role should be created | `bool` | `true` | no | | [deployment\_circuit\_breaker](#input\_deployment\_circuit\_breaker) | Configuration block for deployment circuit breaker |
object({
enable = bool
rollback = bool
})
| `null` | no | -| [deployment\_configuration](#input\_deployment\_configuration) | Configuration block for deployment settings |
object({
strategy = optional(string)
bake_time_in_minutes = optional(string)
canary_configuration = optional(object({
canary_bake_time_in_minutes = optional(string)
canary_percent = optional(string)
}))
linear_configuration = optional(object({
step_bake_time_in_minutes = optional(string)
step_percent = optional(string)
}))
lifecycle_hook = optional(map(object({
hook_target_arn = string
role_arn = string
lifecycle_stages = list(string)
hook_details = optional(string)
})))
})
| `null` | no | +| [deployment\_configuration](#input\_deployment\_configuration) | Configuration block for deployment settings |
object({
strategy = optional(string)
bake_time_in_minutes = optional(string)
canary_configuration = optional(object({
canary_bake_time_in_minutes = optional(string)
canary_percent = optional(string)
}))
linear_configuration = optional(object({
step_bake_time_in_minutes = optional(string)
step_percent = optional(string)
}))
lifecycle_hook = optional(map(object({
hook_target_arn = string
role_arn = optional(string)
lifecycle_stages = list(string)
hook_details = optional(string)
})))
})
| `null` | no | | [deployment\_controller](#input\_deployment\_controller) | Configuration block for deployment controller configuration |
object({
type = optional(string)
})
| `null` | no | | [deployment\_maximum\_percent](#input\_deployment\_maximum\_percent) | Upper limit (as a percentage of the service's `desired_count`) of the number of running tasks that can be running in a service during a deployment | `number` | `200` | no | | [deployment\_minimum\_healthy\_percent](#input\_deployment\_minimum\_healthy\_percent) | Lower limit (as a percentage of the service's `desired_count`) of the number of running tasks that must remain running and healthy in a service during a deployment | `number` | `66` | no | @@ -284,7 +285,7 @@ module "ecs_service" { | [infrastructure\_iam\_role\_use\_name\_prefix](#input\_infrastructure\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`iam_role_name`) is used as a prefix | `bool` | `true` | no | | [ipc\_mode](#input\_ipc\_mode) | IPC resource namespace to be used for the containers in the task The valid values are `host`, `task`, and `none` | `string` | `null` | no | | [launch\_type](#input\_launch\_type) | Launch type on which to run your service. The valid values are `EC2`, `FARGATE`, and `EXTERNAL`. Defaults to `FARGATE` | `string` | `"FARGATE"` | no | -| [load\_balancer](#input\_load\_balancer) | Configuration block for load balancers |
map(object({
container_name = string
container_port = number
elb_name = optional(string)
target_group_arn = optional(string)
advanced_configuration = optional(object({
alternate_target_group_arn = string
production_listener_rule = string
role_arn = string
test_listener_rule = optional(string)
}))
}))
| `null` | no | +| [load\_balancer](#input\_load\_balancer) | Configuration block for load balancers |
map(object({
container_name = string
container_port = number
elb_name = optional(string)
target_group_arn = optional(string)
advanced_configuration = optional(object({
alternate_target_group_arn = string
production_listener_rule = string # Should be optional but bug in provider
role_arn = optional(string)
test_listener_rule = optional(string)
}))
}))
| `null` | no | | [memory](#input\_memory) | Amount (in MiB) of memory used by the task. If the `requires_compatibilities` is `FARGATE` this field is required | `number` | `2048` | no | | [name](#input\_name) | Name of the service (up to 255 letters, numbers, hyphens, and underscores) | `string` | `null` | no | | [network\_mode](#input\_network\_mode) | Docker networking mode to use for the containers in the task. Valid values are `none`, `bridge`, `awsvpc`, and `host` | `string` | `"awsvpc"` | no | diff --git a/modules/service/main.tf b/modules/service/main.tf index b52afb42..ecbf4c0d 100644 --- a/modules/service/main.tf +++ b/modules/service/main.tf @@ -105,7 +105,7 @@ resource "aws_ecs_service" "this" { content { hook_target_arn = lifecycle_hook.value.hook_target_arn - role_arn = lifecycle_hook.value.role_arn + role_arn = try(coalesce(lifecycle_hook.value.role_arn, local.infrastructure_iam_role_arn)) lifecycle_stages = lifecycle_hook.value.lifecycle_stages hook_details = lifecycle_hook.value.hook_details } @@ -148,7 +148,7 @@ resource "aws_ecs_service" "this" { 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 + role_arn = try(coalesce(advanced_configuration.value.role_arn, local.infrastructure_iam_role_arn)) test_listener_rule = advanced_configuration.value.test_listener_rule } } @@ -429,8 +429,8 @@ resource "aws_ecs_service" "ignore_task_definition" { for_each = deployment_configuration.value.linear_configuration != null ? [deployment_configuration.value.linear_configuration] : [] content { - step_percent = linear_configuration.value.step_percent step_bake_time_in_minutes = linear_configuration.value.step_bake_time_in_minutes + step_percent = linear_configuration.value.step_percent } } @@ -438,8 +438,8 @@ resource "aws_ecs_service" "ignore_task_definition" { for_each = deployment_configuration.value.canary_configuration != null ? [deployment_configuration.value.canary_configuration] : [] content { - canary_percent = canary_configuration.value.canary_percent canary_bake_time_in_minutes = canary_configuration.value.canary_bake_time_in_minutes + canary_percent = canary_configuration.value.canary_percent } } @@ -448,7 +448,7 @@ resource "aws_ecs_service" "ignore_task_definition" { content { hook_target_arn = lifecycle_hook.value.hook_target_arn - role_arn = lifecycle_hook.value.role_arn + role_arn = try(coalesce(lifecycle_hook.value.role_arn, local.infrastructure_iam_role_arn)) lifecycle_stages = lifecycle_hook.value.lifecycle_stages hook_details = lifecycle_hook.value.hook_details } @@ -491,7 +491,7 @@ resource "aws_ecs_service" "ignore_task_definition" { 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 + role_arn = try(coalesce(advanced_configuration.value.role_arn, local.infrastructure_iam_role_arn)) test_listener_rule = advanced_configuration.value.test_listener_rule } } @@ -641,6 +641,8 @@ resource "aws_ecs_service" "ignore_task_definition" { } } + sigint_rollback = try(var.deployment_configuration.strategy, null) == "BLUE_GREEN" ? var.sigint_rollback : null + tags = merge(var.tags, var.service_tags) task_definition = local.task_definition triggers = var.triggers @@ -649,7 +651,7 @@ resource "aws_ecs_service" "ignore_task_definition" { for_each = var.volume_configuration != null ? [var.volume_configuration] : [] content { - name = volume_configuration.value.name + name = try(volume_configuration.value.name, volume_configuration.key) dynamic "managed_ebs_volume" { for_each = [volume_configuration.value.managed_ebs_volume] @@ -1950,7 +1952,7 @@ resource "aws_vpc_security_group_egress_rule" "this" { ############################################################################################ locals { - needs_infrastructure_iam_role = var.volume_configuration != null || var.vpc_lattice_configurations != null + needs_infrastructure_iam_role = var.volume_configuration != null || var.vpc_lattice_configurations != null || var.deployment_configuration != null create_infrastructure_iam_role = var.create && var.create_infrastructure_iam_role && local.needs_infrastructure_iam_role infrastructure_iam_role_arn = local.needs_infrastructure_iam_role ? try(aws_iam_role.infrastructure_iam_role[0].arn, var.infrastructure_iam_role_arn) : null infrastructure_iam_role_name = coalesce(var.infrastructure_iam_role_name, var.name, "NotProvided") @@ -1998,3 +2000,10 @@ resource "aws_iam_role_policy_attachment" "infrastructure_iam_role_vpc_lattice_p role = aws_iam_role.infrastructure_iam_role[0].name policy_arn = "arn:${local.partition}:iam::aws:policy/service-role/AmazonECSInfrastructureRolePolicyForVpcLattice" } + +resource "aws_iam_role_policy_attachment" "infrastructure_iam_role_load_balancer_policy" { + count = local.create_infrastructure_iam_role && var.deployment_configuration != null ? 1 : 0 + + role = aws_iam_role.infrastructure_iam_role[0].name + policy_arn = "arn:${local.partition}:iam::aws:policy/AmazonECSInfrastructureRolePolicyForLoadBalancers" +} diff --git a/modules/service/variables.tf b/modules/service/variables.tf index c01c75cb..5c9b281e 100644 --- a/modules/service/variables.tf +++ b/modules/service/variables.tf @@ -93,7 +93,7 @@ variable "deployment_configuration" { })) lifecycle_hook = optional(map(object({ hook_target_arn = string - role_arn = string + role_arn = optional(string) lifecycle_stages = list(string) hook_details = optional(string) }))) @@ -176,8 +176,8 @@ variable "load_balancer" { target_group_arn = optional(string) advanced_configuration = optional(object({ alternate_target_group_arn = string - production_listener_rule = string - role_arn = string + production_listener_rule = string # Should be optional but bug in provider + role_arn = optional(string) test_listener_rule = optional(string) })) })) diff --git a/variables.tf b/variables.tf index 47e4a8b5..739cd8a0 100644 --- a/variables.tf +++ b/variables.tf @@ -651,7 +651,7 @@ variable "services" { })) lifecycle_hook = optional(map(object({ hook_target_arn = string - role_arn = string + role_arn = optional(string) lifecycle_stages = list(string) hook_details = optional(string) }))) @@ -675,8 +675,8 @@ variable "services" { target_group_arn = optional(string) advanced_configuration = optional(object({ alternate_target_group_arn = string - production_listener_rule = string - role_arn = string + production_listener_rule = string # Should be optional but bug in provider + role_arn = optional(string) test_listener_rule = optional(string) })) }))) From 00f040dd8a63b3eb826b2a7121fcb824b9f09f99 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Tue, 16 Dec 2025 20:10:24 -0600 Subject: [PATCH 07/10] chore: Validate and update examples from checking migration path --- README.md | 1 + examples/complete/main.tf | 21 ++++++++++------- examples/ec2-autoscaling/main.tf | 40 ++++++++++++++++++-------------- examples/fargate/README.md | 3 --- examples/fargate/main.tf | 38 +----------------------------- main.tf | 4 ++++ modules/cluster/README.md | 1 + modules/cluster/main.tf | 16 ++++++------- modules/cluster/variables.tf | 6 +++++ modules/service/README.md | 3 ++- modules/service/main.tf | 10 ++++---- modules/service/variables.tf | 8 ++++++- variables.tf | 6 +++++ wrappers/cluster/main.tf | 1 + wrappers/main.tf | 1 + wrappers/service/main.tf | 3 ++- 16 files changed, 79 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index bb4b8850..a5f557e1 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,7 @@ No resources. | [create\_task\_exec\_iam\_role](#input\_create\_task\_exec\_iam\_role) | Determines whether the ECS task definition IAM role should be created | `bool` | `false` | no | | [create\_task\_exec\_policy](#input\_create\_task\_exec\_policy) | Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters | `bool` | `true` | no | | [default\_capacity\_provider\_strategy](#input\_default\_capacity\_provider\_strategy) | Map of default capacity provider strategy definitions to use for the cluster |
map(object({
base = optional(number)
name = optional(string) # Will fall back to use map key if not set
weight = optional(number)
}))
| `null` | no | +| [disable\_default\_name\_postfix](#input\_disable\_default\_name\_postfix) | [DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names | `bool` | `false` | no | | [infrastructure\_iam\_role\_description](#input\_infrastructure\_iam\_role\_description) | Description of the role | `string` | `null` | no | | [infrastructure\_iam\_role\_name](#input\_infrastructure\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | | [infrastructure\_iam\_role\_override\_policy\_documents](#input\_infrastructure\_iam\_role\_override\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid` | `list(string)` | `[]` | no | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 39da4a50..7bc8b48b 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -37,6 +37,7 @@ module "ecs" { cluster_name = local.name # Cluster capacity providers + cluster_capacity_providers = ["FARGATE", "FARGATE_SPOT", "ASG"] default_capacity_provider_strategy = { FARGATE = { weight = 50 @@ -49,15 +50,17 @@ module "ecs" { capacity_providers = { ASG = { - auto_scaling_group_arn = module.autoscaling.autoscaling_group_arn - managed_draining = "ENABLED" - managed_termination_protection = "ENABLED" - - managed_scaling = { - maximum_scaling_step_size = 5 - minimum_scaling_step_size = 1 - status = "ENABLED" - target_capacity = 60 + auto_scaling_group_provider = { + auto_scaling_group_arn = module.autoscaling.autoscaling_group_arn + managed_draining = "ENABLED" + managed_termination_protection = "ENABLED" + + managed_scaling = { + maximum_scaling_step_size = 5 + minimum_scaling_step_size = 1 + status = "ENABLED" + target_capacity = 60 + } } } } diff --git a/examples/ec2-autoscaling/main.tf b/examples/ec2-autoscaling/main.tf index 08791927..369fa1d6 100644 --- a/examples/ec2-autoscaling/main.tf +++ b/examples/ec2-autoscaling/main.tf @@ -50,28 +50,32 @@ module "ecs_cluster" { capacity_providers = { # On-demand instances ex_1 = { - auto_scaling_group_arn = module.autoscaling["ex_1"].autoscaling_group_arn - managed_draining = "ENABLED" - managed_termination_protection = "ENABLED" - - managed_scaling = { - maximum_scaling_step_size = 5 - minimum_scaling_step_size = 1 - status = "ENABLED" - target_capacity = 60 + auto_scaling_group_provider = { + auto_scaling_group_arn = module.autoscaling["ex_1"].autoscaling_group_arn + managed_draining = "ENABLED" + managed_termination_protection = "ENABLED" + + managed_scaling = { + maximum_scaling_step_size = 5 + minimum_scaling_step_size = 1 + status = "ENABLED" + target_capacity = 60 + } } } # Spot instances ex_2 = { - auto_scaling_group_arn = module.autoscaling["ex_2"].autoscaling_group_arn - managed_draining = "ENABLED" - managed_termination_protection = "ENABLED" - - managed_scaling = { - maximum_scaling_step_size = 15 - minimum_scaling_step_size = 5 - status = "ENABLED" - target_capacity = 90 + auto_scaling_group_provider = { + auto_scaling_group_arn = module.autoscaling["ex_2"].autoscaling_group_arn + managed_draining = "ENABLED" + managed_termination_protection = "ENABLED" + + managed_scaling = { + maximum_scaling_step_size = 15 + minimum_scaling_step_size = 5 + status = "ENABLED" + target_capacity = 90 + } } } } diff --git a/examples/fargate/README.md b/examples/fargate/README.md index f8cdeb51..e06e1da7 100644 --- a/examples/fargate/README.md +++ b/examples/fargate/README.md @@ -49,9 +49,6 @@ Note that this example may create resources which will incur monetary charges on | Name | Type | |------|------| -| [aws_iam_role.ecs_elb_permissions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | -| [aws_iam_role_policy_attachment.ecs_elb_management_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | -| [aws_iam_role_policy_attachment.ecs_service_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_service_discovery_http_namespace.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_http_namespace) | resource | | [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | | [aws_ssm_parameter.fluentbit](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | diff --git a/examples/fargate/main.tf b/examples/fargate/main.tf index adabb628..ca74c5cd 100644 --- a/examples/fargate/main.tf +++ b/examples/fargate/main.tf @@ -37,6 +37,7 @@ module "ecs_cluster" { name = local.name # Capacity provider + cluster_capacity_providers = ["FARGATE", "FARGATE_SPOT"] default_capacity_provider_strategy = { FARGATE = { weight = 50 @@ -185,7 +186,6 @@ module "ecs_service" { alternate_target_group_arn = module.alb.target_groups["ex_ecs_alternate"].arn production_listener_rule = module.alb.listener_rules["ex_http/production"].arn test_listener_rule = module.alb.listener_rules["ex_http/test"].arn - role_arn = aws_iam_role.ecs_elb_permissions.arn } } } @@ -211,12 +211,6 @@ module "ecs_service" { } tags = local.tags - - depends_on = [ - aws_iam_role.ecs_elb_permissions, - aws_iam_role_policy_attachment.ecs_service_role, - aws_iam_role_policy_attachment.ecs_elb_management_role - ] } ################################################################################ @@ -449,33 +443,3 @@ module "vpc" { tags = local.tags } - -resource "aws_iam_role" "ecs_elb_permissions" { - name = "${local.name}-ecs-elb-role" - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = [ - "ecs-tasks.amazonaws.com", - "ecs.amazonaws.com", - ] - } - } - ] - }) -} - -# for example purposes only -resource "aws_iam_role_policy_attachment" "ecs_service_role" { - role = aws_iam_role.ecs_elb_permissions.name - policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole" -} - -resource "aws_iam_role_policy_attachment" "ecs_elb_management_role" { - role = aws_iam_role.ecs_elb_permissions.name - policy_arn = "arn:aws:iam::aws:policy/AmazonECSInfrastructureRolePolicyForLoadBalancers" -} diff --git a/main.tf b/main.tf index 7a2c729d..234f473c 100644 --- a/main.tf +++ b/main.tf @@ -8,6 +8,8 @@ module "cluster" { create = var.create region = var.region + disable_default_name_postfix = var.disable_default_name_postfix + # Cluster configuration = var.cluster_configuration name = var.cluster_name @@ -100,6 +102,8 @@ module "service" { create_service = each.value.create_service region = var.region + disable_default_name_postfix = var.disable_default_name_postfix + # Service ignore_task_definition_changes = each.value.ignore_task_definition_changes alarms = each.value.alarms diff --git a/modules/cluster/README.md b/modules/cluster/README.md index 1c4bb6c7..ccee91dd 100644 --- a/modules/cluster/README.md +++ b/modules/cluster/README.md @@ -204,6 +204,7 @@ No modules. | [create\_task\_exec\_iam\_role](#input\_create\_task\_exec\_iam\_role) | Determines whether the ECS task definition IAM role should be created | `bool` | `false` | no | | [create\_task\_exec\_policy](#input\_create\_task\_exec\_policy) | Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters | `bool` | `true` | no | | [default\_capacity\_provider\_strategy](#input\_default\_capacity\_provider\_strategy) | Map of default capacity provider strategy definitions to use for the cluster |
map(object({
base = optional(number)
name = optional(string) # Will fall back to use map key if not set
weight = optional(number)
}))
| `{}` | no | +| [disable\_default\_name\_postfix](#input\_disable\_default\_name\_postfix) | [DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names | `bool` | `false` | no | | [infrastructure\_iam\_role\_description](#input\_infrastructure\_iam\_role\_description) | Description of the role | `string` | `null` | no | | [infrastructure\_iam\_role\_name](#input\_infrastructure\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | | [infrastructure\_iam\_role\_override\_policy\_documents](#input\_infrastructure\_iam\_role\_override\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid` | `list(string)` | `[]` | no | diff --git a/modules/cluster/main.tf b/modules/cluster/main.tf index 67ba104d..b6ad9791 100644 --- a/modules/cluster/main.tf +++ b/modules/cluster/main.tf @@ -154,7 +154,7 @@ resource "aws_ecs_cluster_capacity_providers" "this" { ################################################################################ locals { - managed_instances_enabled = anytrue([for k, v in var.capacity_providers : v.managed_instances_provider != null]) + managed_instances_enabled = var.capacity_providers != null ? anytrue([for k, v in var.capacity_providers : v.managed_instances_provider != null]) : false } resource "aws_ecs_capacity_provider" "this" { @@ -166,11 +166,11 @@ resource "aws_ecs_capacity_provider" "this" { for_each = each.value.auto_scaling_group_provider != null ? [each.value.auto_scaling_group_provider] : [] content { - auto_scaling_group_arn = each.value.auto_scaling_group_arn - managed_draining = each.value.managed_draining + auto_scaling_group_arn = auto_scaling_group_provider.value.auto_scaling_group_arn + managed_draining = auto_scaling_group_provider.value.managed_draining dynamic "managed_scaling" { - for_each = each.value.managed_scaling != null ? [each.value.managed_scaling] : [] + for_each = auto_scaling_group_provider.value.managed_scaling != null ? [auto_scaling_group_provider.value.managed_scaling] : [] content { instance_warmup_period = managed_scaling.value.instance_warmup_period @@ -182,7 +182,7 @@ resource "aws_ecs_capacity_provider" "this" { } # When you use managed termination protection, you must also use managed scaling otherwise managed termination protection won't work - managed_termination_protection = each.value.managed_scaling != null ? each.value.managed_termination_protection : "DISABLED" + managed_termination_protection = auto_scaling_group_provider.value.managed_scaling != null ? auto_scaling_group_provider.value.managed_termination_protection : "DISABLED" } } @@ -354,7 +354,7 @@ resource "aws_ecs_capacity_provider" "this" { ################################################################################ locals { - task_exec_iam_role_name = try(coalesce(var.task_exec_iam_role_name, var.name), "") + task_exec_iam_role_name = try(coalesce(var.task_exec_iam_role_name, "${var.name}${var.disable_default_name_postfix ? "" : "-task-exec"}"), "") create_task_exec_iam_role = var.create && var.create_task_exec_iam_role create_task_exec_policy = local.create_task_exec_iam_role && var.create_task_exec_policy @@ -509,7 +509,7 @@ resource "aws_iam_role_policy_attachment" "task_exec" { locals { create_infrastructure_iam_role = var.create && var.create_infrastructure_iam_role && local.managed_instances_enabled - infrastructure_iam_role_name = coalesce(var.infrastructure_iam_role_name, "${var.name}-infra", "NotProvided") + infrastructure_iam_role_name = coalesce(var.infrastructure_iam_role_name, "${var.name}-infra") } data "aws_iam_policy_document" "infrastructure_assume" { @@ -953,7 +953,7 @@ resource "aws_iam_instance_profile" "this" { locals { create_security_group = var.create && var.create_security_group && local.managed_instances_enabled - security_group_name = coalesce(var.security_group_name, "${var.name}-cluster", "NotProvided") + security_group_name = coalesce(var.security_group_name, "${var.name}${var.disable_default_name_postfix ? "" : "-cluster"}") } resource "aws_security_group" "this" { diff --git a/modules/cluster/variables.tf b/modules/cluster/variables.tf index da84d156..2671c2e3 100644 --- a/modules/cluster/variables.tf +++ b/modules/cluster/variables.tf @@ -16,6 +16,12 @@ variable "tags" { default = {} } +variable "disable_default_name_postfix" { + description = "[DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names" + type = bool + default = false +} + ################################################################################ # Cluster ################################################################################ diff --git a/modules/service/README.md b/modules/service/README.md index ed7730c5..97ad07b4 100644 --- a/modules/service/README.md +++ b/modules/service/README.md @@ -257,6 +257,7 @@ module "ecs_service" { | [deployment\_maximum\_percent](#input\_deployment\_maximum\_percent) | Upper limit (as a percentage of the service's `desired_count`) of the number of running tasks that can be running in a service during a deployment | `number` | `200` | no | | [deployment\_minimum\_healthy\_percent](#input\_deployment\_minimum\_healthy\_percent) | Lower limit (as a percentage of the service's `desired_count`) of the number of running tasks that must remain running and healthy in a service during a deployment | `number` | `66` | no | | [desired\_count](#input\_desired\_count) | Number of instances of the task definition to place and keep running | `number` | `1` | no | +| [disable\_default\_name\_postfix](#input\_disable\_default\_name\_postfix) | [DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names | `bool` | `false` | no | | [enable\_autoscaling](#input\_enable\_autoscaling) | Determines whether to enable autoscaling for the service | `bool` | `true` | no | | [enable\_ecs\_managed\_tags](#input\_enable\_ecs\_managed\_tags) | Specifies whether to enable Amazon ECS managed tags for the tasks within the service | `bool` | `true` | no | | [enable\_execute\_command](#input\_enable\_execute\_command) | Specifies whether to enable Amazon ECS Exec for the tasks within the service | `bool` | `false` | no | @@ -287,7 +288,7 @@ module "ecs_service" { | [launch\_type](#input\_launch\_type) | Launch type on which to run your service. The valid values are `EC2`, `FARGATE`, and `EXTERNAL`. Defaults to `FARGATE` | `string` | `"FARGATE"` | no | | [load\_balancer](#input\_load\_balancer) | Configuration block for load balancers |
map(object({
container_name = string
container_port = number
elb_name = optional(string)
target_group_arn = optional(string)
advanced_configuration = optional(object({
alternate_target_group_arn = string
production_listener_rule = string # Should be optional but bug in provider
role_arn = optional(string)
test_listener_rule = optional(string)
}))
}))
| `null` | no | | [memory](#input\_memory) | Amount (in MiB) of memory used by the task. If the `requires_compatibilities` is `FARGATE` this field is required | `number` | `2048` | no | -| [name](#input\_name) | Name of the service (up to 255 letters, numbers, hyphens, and underscores) | `string` | `null` | no | +| [name](#input\_name) | Name of the service (up to 255 letters, numbers, hyphens, and underscores) | `string` | `""` | no | | [network\_mode](#input\_network\_mode) | Docker networking mode to use for the containers in the task. Valid values are `none`, `bridge`, `awsvpc`, and `host` | `string` | `"awsvpc"` | no | | [ordered\_placement\_strategy](#input\_ordered\_placement\_strategy) | Service level strategy rules that are taken into consideration during task placement. List from top to bottom in order of precedence |
map(object({
field = optional(string)
type = string
}))
| `null` | no | | [pid\_mode](#input\_pid\_mode) | Process namespace to use for the containers in the task. The valid values are `host` and `task` | `string` | `null` | no | diff --git a/modules/service/main.tf b/modules/service/main.tf index ecbf4c0d..0a1dec61 100644 --- a/modules/service/main.tf +++ b/modules/service/main.tf @@ -1059,7 +1059,7 @@ resource "aws_ecs_task_definition" "this" { ################################################################################ locals { - task_exec_iam_role_name = coalesce(var.task_exec_iam_role_name, var.name, "NotProvided") + task_exec_iam_role_name = coalesce(var.task_exec_iam_role_name, "${var.name}${var.disable_default_name_postfix ? "" : "-task-exec"}") create_task_exec_iam_role = local.create_task_definition && var.create_task_exec_iam_role create_task_exec_policy = local.create_task_exec_iam_role && var.create_task_exec_policy @@ -1213,7 +1213,7 @@ resource "aws_iam_role_policy_attachment" "task_exec" { ################################################################################ locals { - tasks_iam_role_name = coalesce(var.tasks_iam_role_name, var.name, "NotProvided") + tasks_iam_role_name = coalesce(var.tasks_iam_role_name, "${var.name}${var.disable_default_name_postfix ? "" : "-tasks"}") create_tasks_iam_role = local.create_task_definition && var.create_tasks_iam_role } @@ -1870,7 +1870,7 @@ resource "aws_appautoscaling_scheduled_action" "this" { locals { create_security_group = var.create && var.create_security_group && var.network_mode == "awsvpc" - security_group_name = coalesce(var.security_group_name, "${var.name}-service", "NotProvided") + security_group_name = coalesce(var.security_group_name, "${var.name}${var.disable_default_name_postfix ? "" : "-service"}") } data "aws_subnet" "this" { @@ -1888,7 +1888,7 @@ resource "aws_security_group" "this" { name = var.security_group_use_name_prefix ? null : local.security_group_name name_prefix = var.security_group_use_name_prefix ? "${local.security_group_name}-" : null - description = coalesce(var.security_group_description, "Security group for ECS Service ${var.name}") + description = try(coalesce(var.security_group_description, (var.disable_default_name_postfix ? null : "Security group for ECS Service ${var.name}")), null) vpc_id = var.vpc_id != null ? var.vpc_id : data.aws_subnet.this[0].vpc_id tags = merge( @@ -1955,7 +1955,7 @@ locals { needs_infrastructure_iam_role = var.volume_configuration != null || var.vpc_lattice_configurations != null || var.deployment_configuration != null create_infrastructure_iam_role = var.create && var.create_infrastructure_iam_role && local.needs_infrastructure_iam_role infrastructure_iam_role_arn = local.needs_infrastructure_iam_role ? try(aws_iam_role.infrastructure_iam_role[0].arn, var.infrastructure_iam_role_arn) : null - infrastructure_iam_role_name = coalesce(var.infrastructure_iam_role_name, var.name, "NotProvided") + infrastructure_iam_role_name = coalesce(var.infrastructure_iam_role_name, "${var.name}${var.disable_default_name_postfix ? "" : "-infra"}") } data "aws_iam_policy_document" "infrastructure_iam_role" { diff --git a/modules/service/variables.tf b/modules/service/variables.tf index 5c9b281e..aa5134f3 100644 --- a/modules/service/variables.tf +++ b/modules/service/variables.tf @@ -25,6 +25,12 @@ variable "tags" { nullable = false } +variable "disable_default_name_postfix" { + description = "[DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names" + type = bool + default = false +} + ################################################################################ # Service ################################################################################ @@ -187,7 +193,7 @@ variable "load_balancer" { variable "name" { description = "Name of the service (up to 255 letters, numbers, hyphens, and underscores)" type = string - default = null + default = "" } variable "assign_public_ip" { diff --git a/variables.tf b/variables.tf index 739cd8a0..6d8e011f 100644 --- a/variables.tf +++ b/variables.tf @@ -16,6 +16,12 @@ variable "tags" { default = {} } +variable "disable_default_name_postfix" { + description = "[DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names" + type = bool + default = false +} + ################################################################################ # Cluster ################################################################################ diff --git a/wrappers/cluster/main.tf b/wrappers/cluster/main.tf index b63075d4..8e7d712a 100644 --- a/wrappers/cluster/main.tf +++ b/wrappers/cluster/main.tf @@ -26,6 +26,7 @@ module "wrapper" { create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, false) create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) default_capacity_provider_strategy = try(each.value.default_capacity_provider_strategy, var.defaults.default_capacity_provider_strategy, {}) + disable_default_name_postfix = try(each.value.disable_default_name_postfix, var.defaults.disable_default_name_postfix, false) infrastructure_iam_role_description = try(each.value.infrastructure_iam_role_description, var.defaults.infrastructure_iam_role_description, null) infrastructure_iam_role_name = try(each.value.infrastructure_iam_role_name, var.defaults.infrastructure_iam_role_name, null) infrastructure_iam_role_override_policy_documents = try(each.value.infrastructure_iam_role_override_policy_documents, var.defaults.infrastructure_iam_role_override_policy_documents, []) diff --git a/wrappers/main.tf b/wrappers/main.tf index 9e171bb6..1cfc7a6b 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -34,6 +34,7 @@ module "wrapper" { create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, false) create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) default_capacity_provider_strategy = try(each.value.default_capacity_provider_strategy, var.defaults.default_capacity_provider_strategy, null) + disable_default_name_postfix = try(each.value.disable_default_name_postfix, var.defaults.disable_default_name_postfix, false) infrastructure_iam_role_description = try(each.value.infrastructure_iam_role_description, var.defaults.infrastructure_iam_role_description, null) infrastructure_iam_role_name = try(each.value.infrastructure_iam_role_name, var.defaults.infrastructure_iam_role_name, null) infrastructure_iam_role_override_policy_documents = try(each.value.infrastructure_iam_role_override_policy_documents, var.defaults.infrastructure_iam_role_override_policy_documents, []) diff --git a/wrappers/service/main.tf b/wrappers/service/main.tf index 44ebf6be..0202f07c 100644 --- a/wrappers/service/main.tf +++ b/wrappers/service/main.tf @@ -49,6 +49,7 @@ module "wrapper" { deployment_maximum_percent = try(each.value.deployment_maximum_percent, var.defaults.deployment_maximum_percent, 200) deployment_minimum_healthy_percent = try(each.value.deployment_minimum_healthy_percent, var.defaults.deployment_minimum_healthy_percent, 66) desired_count = try(each.value.desired_count, var.defaults.desired_count, 1) + disable_default_name_postfix = try(each.value.disable_default_name_postfix, var.defaults.disable_default_name_postfix, false) enable_autoscaling = try(each.value.enable_autoscaling, var.defaults.enable_autoscaling, true) enable_ecs_managed_tags = try(each.value.enable_ecs_managed_tags, var.defaults.enable_ecs_managed_tags, true) enable_execute_command = try(each.value.enable_execute_command, var.defaults.enable_execute_command, false) @@ -79,7 +80,7 @@ module "wrapper" { launch_type = try(each.value.launch_type, var.defaults.launch_type, "FARGATE") load_balancer = try(each.value.load_balancer, var.defaults.load_balancer, null) memory = try(each.value.memory, var.defaults.memory, 2048) - name = try(each.value.name, var.defaults.name, null) + name = try(each.value.name, var.defaults.name, "") network_mode = try(each.value.network_mode, var.defaults.network_mode, "awsvpc") ordered_placement_strategy = try(each.value.ordered_placement_strategy, var.defaults.ordered_placement_strategy, null) pid_mode = try(each.value.pid_mode, var.defaults.pid_mode, null) From 751df793ef93f09f5da93263ee4ad6e2b0556a20 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Tue, 16 Dec 2025 20:15:12 -0600 Subject: [PATCH 08/10] fix: Correct variable type for `ordered_placement_strategy` --- README.md | 2 +- modules/service/README.md | 2 +- modules/service/main.tf | 4 ++-- modules/service/variables.tf | 2 +- variables.tf | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a5f557e1..f07e4bdb 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ No resources. | [security\_group\_name](#input\_security\_group\_name) | Name to use on security group created | `string` | `null` | no | | [security\_group\_tags](#input\_security\_group\_tags) | A map of additional tags to add to the security group created | `map(string)` | `{}` | no | | [security\_group\_use\_name\_prefix](#input\_security\_group\_use\_name\_prefix) | Determines whether the security group name (`security_group_name`) is used as a prefix | `bool` | `true` | no | -| [services](#input\_services) | Map of service definitions to create |
map(object({
create = optional(bool)
create_service = optional(bool)
tags = optional(map(string))

# Service
ignore_task_definition_changes = optional(bool)
alarms = optional(object({
alarm_names = list(string)
enable = optional(bool)
rollback = optional(bool)
}))
availability_zone_rebalancing = optional(string)
capacity_provider_strategy = optional(map(object({
base = optional(number)
capacity_provider = string
weight = optional(number)
})))
deployment_circuit_breaker = optional(object({
enable = bool
rollback = bool
}))
deployment_configuration = optional(object({
strategy = optional(string)
bake_time_in_minutes = optional(string)
canary_configuration = optional(object({
canary_bake_time_in_minutes = optional(string)
canary_percent = optional(string)
}))
linear_configuration = optional(object({
step_bake_time_in_minutes = optional(string)
step_percent = optional(string)
}))
lifecycle_hook = optional(map(object({
hook_target_arn = string
role_arn = optional(string)
lifecycle_stages = list(string)
hook_details = optional(string)
})))
}))
deployment_controller = optional(object({
type = optional(string)
}))
deployment_maximum_percent = optional(number, 200)
deployment_minimum_healthy_percent = optional(number, 66)
desired_count = optional(number, 1)
enable_ecs_managed_tags = optional(bool)
enable_execute_command = optional(bool)
force_delete = optional(bool)
force_new_deployment = optional(bool)
health_check_grace_period_seconds = optional(number)
launch_type = optional(string)
load_balancer = optional(map(object({
container_name = string
container_port = number
elb_name = optional(string)
target_group_arn = optional(string)
advanced_configuration = optional(object({
alternate_target_group_arn = string
production_listener_rule = string # Should be optional but bug in provider
role_arn = optional(string)
test_listener_rule = optional(string)
}))
})))
name = optional(string) # Will fall back to use map key if not set
assign_public_ip = optional(bool)
security_group_ids = optional(list(string))
subnet_ids = optional(list(string))
ordered_placement_strategy = optional(map(object({
field = optional(string)
type = string
})))
placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
platform_version = optional(string)
propagate_tags = optional(string)
scheduling_strategy = optional(string)
service_connect_configuration = optional(object({
enabled = optional(bool)
log_configuration = optional(object({
log_driver = string
options = optional(map(string))
secret_option = optional(list(object({
name = string
value_from = string
})))
}))
namespace = optional(string)
service = optional(list(object({
client_alias = optional(object({
dns_name = optional(string)
port = number
test_traffic_rules = optional(list(object({
header = optional(object({
name = string
value = object({
exact = string
})
}))
})))
}))
discovery_name = optional(string)
ingress_port_override = optional(number)
port_name = string
timeout = optional(object({
idle_timeout_seconds = optional(number)
per_request_timeout_seconds = optional(number)
}))
tls = optional(object({
issuer_cert_authority = object({
aws_pca_authority_arn = string
})
kms_key = optional(string)
role_arn = optional(string)
}))
})))
}))
service_registries = optional(object({
container_name = optional(string)
container_port = optional(number)
port = optional(number)
registry_arn = string
}))
sigint_rollback = optional(bool)
timeouts = optional(object({
create = optional(string)
update = optional(string)
delete = optional(string)
}))
triggers = optional(map(string))
volume_configuration = optional(object({
name = string
managed_ebs_volume = object({
encrypted = optional(bool)
file_system_type = optional(string)
iops = optional(number)
kms_key_id = optional(string)
size_in_gb = optional(number)
snapshot_id = optional(string)
tag_specifications = optional(list(object({
propagate_tags = optional(string)
resource_type = string
tags = optional(map(string))
})))
throughput = optional(number)
volume_type = optional(string)
})
}))
vpc_lattice_configurations = optional(object({
role_arn = string
target_group_arn = string
port_name = string
}))
wait_for_steady_state = optional(bool)
service_tags = optional(map(string))
# Service - IAM Role
create_iam_role = optional(bool)
iam_role_arn = optional(string)
iam_role_name = optional(string)
iam_role_use_name_prefix = optional(bool)
iam_role_path = optional(string)
iam_role_description = optional(string)
iam_role_permissions_boundary = optional(string)
iam_role_tags = optional(map(string))
iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
# Task Definition
create_task_definition = optional(bool)
task_definition_arn = optional(string)
container_definitions = optional(map(object({
operating_system_family = optional(string)
tags = optional(map(string))

# Container definition
command = optional(list(string))
cpu = optional(number)
credentialSpecs = optional(list(string))
dependsOn = optional(list(object({
condition = string
containerName = string
})))
disableNetworking = optional(bool)
dnsSearchDomains = optional(list(string))
dnsServers = optional(list(string))
dockerLabels = optional(map(string))
dockerSecurityOptions = optional(list(string))
enable_execute_command = optional(bool)
entrypoint = optional(list(string))
environment = optional(list(object({
name = string
value = string
})))
environmentFiles = optional(list(object({
type = string
value = string
})))
essential = optional(bool)
extraHosts = optional(list(object({
hostname = string
ipAddress = string
})))
firelensConfiguration = optional(object({
options = optional(map(string))
type = optional(string)
}))
healthCheck = optional(object({
command = optional(list(string))
interval = optional(number)
retries = optional(number)
startPeriod = optional(number)
timeout = optional(number)
}))
hostname = optional(string)
image = optional(string)
interactive = optional(bool)
links = optional(list(string))
linuxParameters = optional(object({
capabilities = optional(object({
add = optional(list(string))
drop = optional(list(string))
}))
devices = optional(list(object({
containerPath = optional(string)
hostPath = optional(string)
permissions = optional(list(string))
})))
initProcessEnabled = optional(bool)
maxSwap = optional(number)
sharedMemorySize = optional(number)
swappiness = optional(number)
tmpfs = optional(list(object({
containerPath = string
mountOptions = optional(list(string))
size = number
})))
}))
logConfiguration = optional(object({
logDriver = optional(string)
options = optional(map(string))
secretOptions = optional(list(object({
name = string
valueFrom = string
})))
}))
memory = optional(number)
memoryReservation = optional(number)
mountPoints = optional(list(object({
containerPath = optional(string)
readOnly = optional(bool)
sourceVolume = optional(string)
})), [])
name = optional(string)
portMappings = optional(list(object({
appProtocol = optional(string)
containerPort = optional(number)
containerPortRange = optional(string)
hostPort = optional(number)
name = optional(string)
protocol = optional(string)
})), [])
privileged = optional(bool)
pseudoTerminal = optional(bool)
readonlyRootFilesystem = optional(bool)
repositoryCredentials = optional(object({
credentialsParameter = optional(string)
}))
resourceRequirements = optional(list(object({
type = string
value = string
})))
restartPolicy = optional(object({
enabled = optional(bool)
ignoredExitCodes = optional(list(number))
restartAttemptPeriod = optional(number)
}))
secrets = optional(list(object({
name = string
valueFrom = string
})))
startTimeout = optional(number)
stopTimeout = optional(number)
systemControls = optional(list(object({
namespace = optional(string)
value = optional(string)
})))
ulimits = optional(list(object({
hardLimit = number
name = string
softLimit = number
})))
user = optional(string)
versionConsistency = optional(string)
volumesFrom = optional(list(object({
readOnly = optional(bool)
sourceContainer = optional(string)
})))
workingDirectory = optional(string)

# Cloudwatch Log Group
service = optional(string, "")
enable_cloudwatch_logging = optional(bool)
create_cloudwatch_log_group = optional(bool)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool)
cloudwatch_log_group_class = optional(string)
cloudwatch_log_group_retention_in_days = optional(number)
cloudwatch_log_group_kms_key_id = optional(string)
})))
cpu = optional(number, 1024)
enable_fault_injection = optional(bool)
ephemeral_storage = optional(object({
size_in_gib = number
}))
family = optional(string)
ipc_mode = optional(string)
memory = optional(number, 2048)
network_mode = optional(string)
pid_mode = optional(string)
proxy_configuration = optional(object({
container_name = string
properties = optional(map(string))
type = optional(string)
}))
requires_compatibilities = optional(list(string))
runtime_platform = optional(object({
cpu_architecture = optional(string)
operating_system_family = optional(string)
}))
skip_destroy = optional(bool)
task_definition_placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
track_latest = optional(bool)
volume = optional(map(object({
configure_at_launch = optional(bool)
docker_volume_configuration = optional(object({
autoprovision = optional(bool)
driver = optional(string)
driver_opts = optional(map(string))
labels = optional(map(string))
scope = optional(string)
}))
efs_volume_configuration = optional(object({
authorization_config = optional(object({
access_point_id = optional(string)
iam = optional(string)
}))
file_system_id = string
root_directory = optional(string)
transit_encryption = optional(string)
transit_encryption_port = optional(number)
}))
fsx_windows_file_server_volume_configuration = optional(object({
authorization_config = optional(object({
credentials_parameter = string
domain = string
}))
file_system_id = string
root_directory = string
}))
host_path = optional(string)
name = optional(string)
})))
task_tags = optional(map(string))
# Task Execution - IAM Role
create_task_exec_iam_role = optional(bool)
task_exec_iam_role_arn = optional(string)
task_exec_iam_role_name = optional(string)
task_exec_iam_role_use_name_prefix = optional(bool)
task_exec_iam_role_path = optional(string)
task_exec_iam_role_description = optional(string)
task_exec_iam_role_permissions_boundary = optional(string)
task_exec_iam_role_tags = optional(map(string))
task_exec_iam_role_policies = optional(map(string))
task_exec_iam_role_max_session_duration = optional(number)
create_task_exec_policy = optional(bool)
task_exec_ssm_param_arns = optional(list(string))
task_exec_secret_arns = optional(list(string))
task_exec_iam_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
task_exec_iam_policy_path = optional(string)
# Tasks - IAM Role
create_tasks_iam_role = optional(bool)
tasks_iam_role_arn = optional(string)
tasks_iam_role_name = optional(string)
tasks_iam_role_use_name_prefix = optional(bool)
tasks_iam_role_path = optional(string)
tasks_iam_role_description = optional(string)
tasks_iam_role_permissions_boundary = optional(string)
tasks_iam_role_tags = optional(map(string))
tasks_iam_role_policies = optional(map(string))
tasks_iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
# Task Set
external_id = optional(string)
scale = optional(object({
unit = optional(string)
value = optional(number)
}))
wait_until_stable = optional(bool)
wait_until_stable_timeout = optional(string)
# Autoscaling
enable_autoscaling = optional(bool)
autoscaling_min_capacity = optional(number)
autoscaling_max_capacity = optional(number)
autoscaling_policies = optional(map(object({
name = optional(string) # Will fall back to the key name if not provided
policy_type = optional(string)
predictive_scaling_policy_configuration = optional(object({
max_capacity_breach_behavior = optional(string)
max_capacity_buffer = optional(number)
metric_specification = list(object({
customized_capacity_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_load_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_scaling_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
predefined_load_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_metric_pair_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_scaling_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
target_value = number
}))
mode = optional(string)
scheduling_buffer_time = optional(number)
}))
step_scaling_policy_configuration = optional(object({
adjustment_type = optional(string)
cooldown = optional(number)
metric_aggregation_type = optional(string)
min_adjustment_magnitude = optional(number)
step_adjustment = optional(list(object({
metric_interval_lower_bound = optional(string)
metric_interval_upper_bound = optional(string)
scaling_adjustment = number
})))
}))
target_tracking_scaling_policy_configuration = optional(object({
customized_metric_specification = optional(object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
metrics = optional(list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = string
namespace = string
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
})))
namespace = optional(string)
statistic = optional(string)
unit = optional(string)
}))

disable_scale_in = optional(bool)
predefined_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
scale_in_cooldown = optional(number)
scale_out_cooldown = optional(number)
target_value = optional(number)
}))
})))
autoscaling_scheduled_actions = optional(map(object({
name = optional(string)
min_capacity = number
max_capacity = number
schedule = string
start_time = optional(string)
end_time = optional(string)
timezone = optional(string)
})))
autoscaling_suspended_state = optional(object({
dynamic_scaling_in_suspended = optional(bool)
dynamic_scaling_out_suspended = optional(bool)
scheduled_scaling_suspended = optional(bool)
}))
# Security Group
create_security_group = optional(bool)
vpc_id = optional(string)
security_group_name = optional(string)
security_group_use_name_prefix = optional(bool)
security_group_description = optional(string)
security_group_ingress_rules = optional(map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string))
to_port = optional(string)
})))
security_group_egress_rules = optional(map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string))
to_port = optional(string)
})))
security_group_tags = optional(map(string))
# ECS Infrastructure IAM Role
create_infrastructure_iam_role = optional(bool)
infrastructure_iam_role_arn = optional(string)
infrastructure_iam_role_name = optional(string)
infrastructure_iam_role_use_name_prefix = optional(bool)
infrastructure_iam_role_path = optional(string)
infrastructure_iam_role_description = optional(string)
infrastructure_iam_role_permissions_boundary = optional(string)
infrastructure_iam_role_tags = optional(map(string))
}))
| `null` | no | +| [services](#input\_services) | Map of service definitions to create |
map(object({
create = optional(bool)
create_service = optional(bool)
tags = optional(map(string))

# Service
ignore_task_definition_changes = optional(bool)
alarms = optional(object({
alarm_names = list(string)
enable = optional(bool)
rollback = optional(bool)
}))
availability_zone_rebalancing = optional(string)
capacity_provider_strategy = optional(map(object({
base = optional(number)
capacity_provider = string
weight = optional(number)
})))
deployment_circuit_breaker = optional(object({
enable = bool
rollback = bool
}))
deployment_configuration = optional(object({
strategy = optional(string)
bake_time_in_minutes = optional(string)
canary_configuration = optional(object({
canary_bake_time_in_minutes = optional(string)
canary_percent = optional(string)
}))
linear_configuration = optional(object({
step_bake_time_in_minutes = optional(string)
step_percent = optional(string)
}))
lifecycle_hook = optional(map(object({
hook_target_arn = string
role_arn = optional(string)
lifecycle_stages = list(string)
hook_details = optional(string)
})))
}))
deployment_controller = optional(object({
type = optional(string)
}))
deployment_maximum_percent = optional(number, 200)
deployment_minimum_healthy_percent = optional(number, 66)
desired_count = optional(number, 1)
enable_ecs_managed_tags = optional(bool)
enable_execute_command = optional(bool)
force_delete = optional(bool)
force_new_deployment = optional(bool)
health_check_grace_period_seconds = optional(number)
launch_type = optional(string)
load_balancer = optional(map(object({
container_name = string
container_port = number
elb_name = optional(string)
target_group_arn = optional(string)
advanced_configuration = optional(object({
alternate_target_group_arn = string
production_listener_rule = string # Should be optional but bug in provider
role_arn = optional(string)
test_listener_rule = optional(string)
}))
})))
name = optional(string) # Will fall back to use map key if not set
assign_public_ip = optional(bool)
security_group_ids = optional(list(string))
subnet_ids = optional(list(string))
ordered_placement_strategy = optional(list(object({
field = optional(string)
type = string
})))
placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
platform_version = optional(string)
propagate_tags = optional(string)
scheduling_strategy = optional(string)
service_connect_configuration = optional(object({
enabled = optional(bool)
log_configuration = optional(object({
log_driver = string
options = optional(map(string))
secret_option = optional(list(object({
name = string
value_from = string
})))
}))
namespace = optional(string)
service = optional(list(object({
client_alias = optional(object({
dns_name = optional(string)
port = number
test_traffic_rules = optional(list(object({
header = optional(object({
name = string
value = object({
exact = string
})
}))
})))
}))
discovery_name = optional(string)
ingress_port_override = optional(number)
port_name = string
timeout = optional(object({
idle_timeout_seconds = optional(number)
per_request_timeout_seconds = optional(number)
}))
tls = optional(object({
issuer_cert_authority = object({
aws_pca_authority_arn = string
})
kms_key = optional(string)
role_arn = optional(string)
}))
})))
}))
service_registries = optional(object({
container_name = optional(string)
container_port = optional(number)
port = optional(number)
registry_arn = string
}))
sigint_rollback = optional(bool)
timeouts = optional(object({
create = optional(string)
update = optional(string)
delete = optional(string)
}))
triggers = optional(map(string))
volume_configuration = optional(object({
name = string
managed_ebs_volume = object({
encrypted = optional(bool)
file_system_type = optional(string)
iops = optional(number)
kms_key_id = optional(string)
size_in_gb = optional(number)
snapshot_id = optional(string)
tag_specifications = optional(list(object({
propagate_tags = optional(string)
resource_type = string
tags = optional(map(string))
})))
throughput = optional(number)
volume_type = optional(string)
})
}))
vpc_lattice_configurations = optional(object({
role_arn = string
target_group_arn = string
port_name = string
}))
wait_for_steady_state = optional(bool)
service_tags = optional(map(string))
# Service - IAM Role
create_iam_role = optional(bool)
iam_role_arn = optional(string)
iam_role_name = optional(string)
iam_role_use_name_prefix = optional(bool)
iam_role_path = optional(string)
iam_role_description = optional(string)
iam_role_permissions_boundary = optional(string)
iam_role_tags = optional(map(string))
iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
# Task Definition
create_task_definition = optional(bool)
task_definition_arn = optional(string)
container_definitions = optional(map(object({
operating_system_family = optional(string)
tags = optional(map(string))

# Container definition
command = optional(list(string))
cpu = optional(number)
credentialSpecs = optional(list(string))
dependsOn = optional(list(object({
condition = string
containerName = string
})))
disableNetworking = optional(bool)
dnsSearchDomains = optional(list(string))
dnsServers = optional(list(string))
dockerLabels = optional(map(string))
dockerSecurityOptions = optional(list(string))
enable_execute_command = optional(bool)
entrypoint = optional(list(string))
environment = optional(list(object({
name = string
value = string
})))
environmentFiles = optional(list(object({
type = string
value = string
})))
essential = optional(bool)
extraHosts = optional(list(object({
hostname = string
ipAddress = string
})))
firelensConfiguration = optional(object({
options = optional(map(string))
type = optional(string)
}))
healthCheck = optional(object({
command = optional(list(string))
interval = optional(number)
retries = optional(number)
startPeriod = optional(number)
timeout = optional(number)
}))
hostname = optional(string)
image = optional(string)
interactive = optional(bool)
links = optional(list(string))
linuxParameters = optional(object({
capabilities = optional(object({
add = optional(list(string))
drop = optional(list(string))
}))
devices = optional(list(object({
containerPath = optional(string)
hostPath = optional(string)
permissions = optional(list(string))
})))
initProcessEnabled = optional(bool)
maxSwap = optional(number)
sharedMemorySize = optional(number)
swappiness = optional(number)
tmpfs = optional(list(object({
containerPath = string
mountOptions = optional(list(string))
size = number
})))
}))
logConfiguration = optional(object({
logDriver = optional(string)
options = optional(map(string))
secretOptions = optional(list(object({
name = string
valueFrom = string
})))
}))
memory = optional(number)
memoryReservation = optional(number)
mountPoints = optional(list(object({
containerPath = optional(string)
readOnly = optional(bool)
sourceVolume = optional(string)
})), [])
name = optional(string)
portMappings = optional(list(object({
appProtocol = optional(string)
containerPort = optional(number)
containerPortRange = optional(string)
hostPort = optional(number)
name = optional(string)
protocol = optional(string)
})), [])
privileged = optional(bool)
pseudoTerminal = optional(bool)
readonlyRootFilesystem = optional(bool)
repositoryCredentials = optional(object({
credentialsParameter = optional(string)
}))
resourceRequirements = optional(list(object({
type = string
value = string
})))
restartPolicy = optional(object({
enabled = optional(bool)
ignoredExitCodes = optional(list(number))
restartAttemptPeriod = optional(number)
}))
secrets = optional(list(object({
name = string
valueFrom = string
})))
startTimeout = optional(number)
stopTimeout = optional(number)
systemControls = optional(list(object({
namespace = optional(string)
value = optional(string)
})))
ulimits = optional(list(object({
hardLimit = number
name = string
softLimit = number
})))
user = optional(string)
versionConsistency = optional(string)
volumesFrom = optional(list(object({
readOnly = optional(bool)
sourceContainer = optional(string)
})))
workingDirectory = optional(string)

# Cloudwatch Log Group
service = optional(string, "")
enable_cloudwatch_logging = optional(bool)
create_cloudwatch_log_group = optional(bool)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool)
cloudwatch_log_group_class = optional(string)
cloudwatch_log_group_retention_in_days = optional(number)
cloudwatch_log_group_kms_key_id = optional(string)
})))
cpu = optional(number, 1024)
enable_fault_injection = optional(bool)
ephemeral_storage = optional(object({
size_in_gib = number
}))
family = optional(string)
ipc_mode = optional(string)
memory = optional(number, 2048)
network_mode = optional(string)
pid_mode = optional(string)
proxy_configuration = optional(object({
container_name = string
properties = optional(map(string))
type = optional(string)
}))
requires_compatibilities = optional(list(string))
runtime_platform = optional(object({
cpu_architecture = optional(string)
operating_system_family = optional(string)
}))
skip_destroy = optional(bool)
task_definition_placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
track_latest = optional(bool)
volume = optional(map(object({
configure_at_launch = optional(bool)
docker_volume_configuration = optional(object({
autoprovision = optional(bool)
driver = optional(string)
driver_opts = optional(map(string))
labels = optional(map(string))
scope = optional(string)
}))
efs_volume_configuration = optional(object({
authorization_config = optional(object({
access_point_id = optional(string)
iam = optional(string)
}))
file_system_id = string
root_directory = optional(string)
transit_encryption = optional(string)
transit_encryption_port = optional(number)
}))
fsx_windows_file_server_volume_configuration = optional(object({
authorization_config = optional(object({
credentials_parameter = string
domain = string
}))
file_system_id = string
root_directory = string
}))
host_path = optional(string)
name = optional(string)
})))
task_tags = optional(map(string))
# Task Execution - IAM Role
create_task_exec_iam_role = optional(bool)
task_exec_iam_role_arn = optional(string)
task_exec_iam_role_name = optional(string)
task_exec_iam_role_use_name_prefix = optional(bool)
task_exec_iam_role_path = optional(string)
task_exec_iam_role_description = optional(string)
task_exec_iam_role_permissions_boundary = optional(string)
task_exec_iam_role_tags = optional(map(string))
task_exec_iam_role_policies = optional(map(string))
task_exec_iam_role_max_session_duration = optional(number)
create_task_exec_policy = optional(bool)
task_exec_ssm_param_arns = optional(list(string))
task_exec_secret_arns = optional(list(string))
task_exec_iam_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
task_exec_iam_policy_path = optional(string)
# Tasks - IAM Role
create_tasks_iam_role = optional(bool)
tasks_iam_role_arn = optional(string)
tasks_iam_role_name = optional(string)
tasks_iam_role_use_name_prefix = optional(bool)
tasks_iam_role_path = optional(string)
tasks_iam_role_description = optional(string)
tasks_iam_role_permissions_boundary = optional(string)
tasks_iam_role_tags = optional(map(string))
tasks_iam_role_policies = optional(map(string))
tasks_iam_role_statements = optional(list(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})))
# Task Set
external_id = optional(string)
scale = optional(object({
unit = optional(string)
value = optional(number)
}))
wait_until_stable = optional(bool)
wait_until_stable_timeout = optional(string)
# Autoscaling
enable_autoscaling = optional(bool)
autoscaling_min_capacity = optional(number)
autoscaling_max_capacity = optional(number)
autoscaling_policies = optional(map(object({
name = optional(string) # Will fall back to the key name if not provided
policy_type = optional(string)
predictive_scaling_policy_configuration = optional(object({
max_capacity_breach_behavior = optional(string)
max_capacity_buffer = optional(number)
metric_specification = list(object({
customized_capacity_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_load_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_scaling_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
predefined_load_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_metric_pair_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_scaling_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
target_value = number
}))
mode = optional(string)
scheduling_buffer_time = optional(number)
}))
step_scaling_policy_configuration = optional(object({
adjustment_type = optional(string)
cooldown = optional(number)
metric_aggregation_type = optional(string)
min_adjustment_magnitude = optional(number)
step_adjustment = optional(list(object({
metric_interval_lower_bound = optional(string)
metric_interval_upper_bound = optional(string)
scaling_adjustment = number
})))
}))
target_tracking_scaling_policy_configuration = optional(object({
customized_metric_specification = optional(object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
metrics = optional(list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = string
namespace = string
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
})))
namespace = optional(string)
statistic = optional(string)
unit = optional(string)
}))

disable_scale_in = optional(bool)
predefined_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
scale_in_cooldown = optional(number)
scale_out_cooldown = optional(number)
target_value = optional(number)
}))
})))
autoscaling_scheduled_actions = optional(map(object({
name = optional(string)
min_capacity = number
max_capacity = number
schedule = string
start_time = optional(string)
end_time = optional(string)
timezone = optional(string)
})))
autoscaling_suspended_state = optional(object({
dynamic_scaling_in_suspended = optional(bool)
dynamic_scaling_out_suspended = optional(bool)
scheduled_scaling_suspended = optional(bool)
}))
# Security Group
create_security_group = optional(bool)
vpc_id = optional(string)
security_group_name = optional(string)
security_group_use_name_prefix = optional(bool)
security_group_description = optional(string)
security_group_ingress_rules = optional(map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string))
to_port = optional(string)
})))
security_group_egress_rules = optional(map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string))
to_port = optional(string)
})))
security_group_tags = optional(map(string))
# ECS Infrastructure IAM Role
create_infrastructure_iam_role = optional(bool)
infrastructure_iam_role_arn = optional(string)
infrastructure_iam_role_name = optional(string)
infrastructure_iam_role_use_name_prefix = optional(bool)
infrastructure_iam_role_path = optional(string)
infrastructure_iam_role_description = optional(string)
infrastructure_iam_role_permissions_boundary = optional(string)
infrastructure_iam_role_tags = optional(map(string))
}))
| `null` | no | | [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | | [task\_exec\_iam\_role\_description](#input\_task\_exec\_iam\_role\_description) | Description of the role | `string` | `null` | no | | [task\_exec\_iam\_role\_name](#input\_task\_exec\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | diff --git a/modules/service/README.md b/modules/service/README.md index 97ad07b4..6287972c 100644 --- a/modules/service/README.md +++ b/modules/service/README.md @@ -290,7 +290,7 @@ module "ecs_service" { | [memory](#input\_memory) | Amount (in MiB) of memory used by the task. If the `requires_compatibilities` is `FARGATE` this field is required | `number` | `2048` | no | | [name](#input\_name) | Name of the service (up to 255 letters, numbers, hyphens, and underscores) | `string` | `""` | no | | [network\_mode](#input\_network\_mode) | Docker networking mode to use for the containers in the task. Valid values are `none`, `bridge`, `awsvpc`, and `host` | `string` | `"awsvpc"` | no | -| [ordered\_placement\_strategy](#input\_ordered\_placement\_strategy) | Service level strategy rules that are taken into consideration during task placement. List from top to bottom in order of precedence |
map(object({
field = optional(string)
type = string
}))
| `null` | no | +| [ordered\_placement\_strategy](#input\_ordered\_placement\_strategy) | Service level strategy rules that are taken into consideration during task placement. List from top to bottom in order of precedence |
list(object({
field = optional(string)
type = string
}))
| `null` | no | | [pid\_mode](#input\_pid\_mode) | Process namespace to use for the containers in the task. The valid values are `host` and `task` | `string` | `null` | no | | [placement\_constraints](#input\_placement\_constraints) | Configuration block for rules that are taken into consideration during task placement (up to max of 10). This is set at the service, see `task_definition_placement_constraints` for setting at the task definition |
map(object({
expression = optional(string)
type = string
}))
| `null` | no | | [platform\_version](#input\_platform\_version) | Platform version on which to run your service. Only applicable for `launch_type` set to `FARGATE`. Defaults to `LATEST` | `string` | `null` | no | diff --git a/modules/service/main.tf b/modules/service/main.tf index 0a1dec61..cdcc9237 100644 --- a/modules/service/main.tf +++ b/modules/service/main.tf @@ -169,7 +169,7 @@ resource "aws_ecs_service" "this" { } dynamic "ordered_placement_strategy" { - for_each = var.ordered_placement_strategy != null ? var.ordered_placement_strategy : {} + for_each = var.ordered_placement_strategy != null ? var.ordered_placement_strategy : [] content { field = ordered_placement_strategy.value.field @@ -512,7 +512,7 @@ resource "aws_ecs_service" "ignore_task_definition" { } dynamic "ordered_placement_strategy" { - for_each = var.ordered_placement_strategy != null ? var.ordered_placement_strategy : {} + for_each = var.ordered_placement_strategy != null ? var.ordered_placement_strategy : [] content { field = ordered_placement_strategy.value.field diff --git a/modules/service/variables.tf b/modules/service/variables.tf index aa5134f3..a022a819 100644 --- a/modules/service/variables.tf +++ b/modules/service/variables.tf @@ -231,7 +231,7 @@ variable "vpc_id" { variable "ordered_placement_strategy" { description = "Service level strategy rules that are taken into consideration during task placement. List from top to bottom in order of precedence" - type = map(object({ + type = list(object({ field = optional(string) type = string })) diff --git a/variables.tf b/variables.tf index 6d8e011f..ba03aa8b 100644 --- a/variables.tf +++ b/variables.tf @@ -690,7 +690,7 @@ variable "services" { assign_public_ip = optional(bool) security_group_ids = optional(list(string)) subnet_ids = optional(list(string)) - ordered_placement_strategy = optional(map(object({ + ordered_placement_strategy = optional(list(object({ field = optional(string) type = string }))) From 024329f5c5d5f7136080415b7a8584a17ac68cf5 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Wed, 17 Dec 2025 11:12:51 -0600 Subject: [PATCH 09/10] chore: Update variable name for v7 default name/description changes --- README.md | 2 +- main.tf | 4 ++-- modules/cluster/README.md | 2 +- modules/cluster/main.tf | 6 +++--- modules/cluster/variables.tf | 2 +- modules/service/README.md | 2 +- modules/service/main.tf | 14 +++++++------- modules/service/variables.tf | 2 +- variables.tf | 4 ++-- wrappers/cluster/main.tf | 2 +- wrappers/main.tf | 2 +- wrappers/service/main.tf | 2 +- 12 files changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index f07e4bdb..ce838770 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ No resources. | [create\_task\_exec\_iam\_role](#input\_create\_task\_exec\_iam\_role) | Determines whether the ECS task definition IAM role should be created | `bool` | `false` | no | | [create\_task\_exec\_policy](#input\_create\_task\_exec\_policy) | Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters | `bool` | `true` | no | | [default\_capacity\_provider\_strategy](#input\_default\_capacity\_provider\_strategy) | Map of default capacity provider strategy definitions to use for the cluster |
map(object({
base = optional(number)
name = optional(string) # Will fall back to use map key if not set
weight = optional(number)
}))
| `null` | no | -| [disable\_default\_name\_postfix](#input\_disable\_default\_name\_postfix) | [DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names | `bool` | `false` | no | +| [disable\_v7\_default\_name\_description](#input\_disable\_v7\_default\_name\_description) | [DEPRECATED - will be removed in v9.0] Determines whether to disable the default postfix added to resource names and descriptions added in v7.0 | `bool` | `false` | no | | [infrastructure\_iam\_role\_description](#input\_infrastructure\_iam\_role\_description) | Description of the role | `string` | `null` | no | | [infrastructure\_iam\_role\_name](#input\_infrastructure\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | | [infrastructure\_iam\_role\_override\_policy\_documents](#input\_infrastructure\_iam\_role\_override\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid` | `list(string)` | `[]` | no | diff --git a/main.tf b/main.tf index 234f473c..cf9210b6 100644 --- a/main.tf +++ b/main.tf @@ -8,7 +8,7 @@ module "cluster" { create = var.create region = var.region - disable_default_name_postfix = var.disable_default_name_postfix + disable_v7_default_name_description = var.disable_v7_default_name_description # Cluster configuration = var.cluster_configuration @@ -102,7 +102,7 @@ module "service" { create_service = each.value.create_service region = var.region - disable_default_name_postfix = var.disable_default_name_postfix + disable_v7_default_name_description = var.disable_v7_default_name_description # Service ignore_task_definition_changes = each.value.ignore_task_definition_changes diff --git a/modules/cluster/README.md b/modules/cluster/README.md index ccee91dd..fd6bbd83 100644 --- a/modules/cluster/README.md +++ b/modules/cluster/README.md @@ -204,7 +204,7 @@ No modules. | [create\_task\_exec\_iam\_role](#input\_create\_task\_exec\_iam\_role) | Determines whether the ECS task definition IAM role should be created | `bool` | `false` | no | | [create\_task\_exec\_policy](#input\_create\_task\_exec\_policy) | Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters | `bool` | `true` | no | | [default\_capacity\_provider\_strategy](#input\_default\_capacity\_provider\_strategy) | Map of default capacity provider strategy definitions to use for the cluster |
map(object({
base = optional(number)
name = optional(string) # Will fall back to use map key if not set
weight = optional(number)
}))
| `{}` | no | -| [disable\_default\_name\_postfix](#input\_disable\_default\_name\_postfix) | [DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names | `bool` | `false` | no | +| [disable\_v7\_default\_name\_description](#input\_disable\_v7\_default\_name\_description) | [DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names | `bool` | `false` | no | | [infrastructure\_iam\_role\_description](#input\_infrastructure\_iam\_role\_description) | Description of the role | `string` | `null` | no | | [infrastructure\_iam\_role\_name](#input\_infrastructure\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | | [infrastructure\_iam\_role\_override\_policy\_documents](#input\_infrastructure\_iam\_role\_override\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid` | `list(string)` | `[]` | no | diff --git a/modules/cluster/main.tf b/modules/cluster/main.tf index b6ad9791..74c2d7df 100644 --- a/modules/cluster/main.tf +++ b/modules/cluster/main.tf @@ -354,7 +354,7 @@ resource "aws_ecs_capacity_provider" "this" { ################################################################################ locals { - task_exec_iam_role_name = try(coalesce(var.task_exec_iam_role_name, "${var.name}${var.disable_default_name_postfix ? "" : "-task-exec"}"), "") + task_exec_iam_role_name = try(coalesce(var.task_exec_iam_role_name, "${var.name}${var.disable_v7_default_name_description ? "" : "-task-exec"}"), "") create_task_exec_iam_role = var.create && var.create_task_exec_iam_role create_task_exec_policy = local.create_task_exec_iam_role && var.create_task_exec_policy @@ -794,7 +794,7 @@ resource "aws_iam_role" "node" { name = var.node_iam_role_use_name_prefix ? null : local.node_iam_role_name name_prefix = var.node_iam_role_use_name_prefix ? "${local.node_iam_role_name}-" : null path = var.node_iam_role_path - description = var.node_iam_role_description + description = coalesce(var.node_iam_role_description, "Amazon ECS managed instance node role for ECS cluster ${var.name}") assume_role_policy = data.aws_iam_policy_document.node_assume_role_policy[0].json permissions_boundary = var.node_iam_role_permissions_boundary @@ -953,7 +953,7 @@ resource "aws_iam_instance_profile" "this" { locals { create_security_group = var.create && var.create_security_group && local.managed_instances_enabled - security_group_name = coalesce(var.security_group_name, "${var.name}${var.disable_default_name_postfix ? "" : "-cluster"}") + security_group_name = coalesce(var.security_group_name, "${var.name}${var.disable_v7_default_name_description ? "" : "-cluster"}") } resource "aws_security_group" "this" { diff --git a/modules/cluster/variables.tf b/modules/cluster/variables.tf index 2671c2e3..a7e49761 100644 --- a/modules/cluster/variables.tf +++ b/modules/cluster/variables.tf @@ -16,7 +16,7 @@ variable "tags" { default = {} } -variable "disable_default_name_postfix" { +variable "disable_v7_default_name_description" { description = "[DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names" type = bool default = false diff --git a/modules/service/README.md b/modules/service/README.md index 6287972c..6152ce48 100644 --- a/modules/service/README.md +++ b/modules/service/README.md @@ -257,7 +257,7 @@ module "ecs_service" { | [deployment\_maximum\_percent](#input\_deployment\_maximum\_percent) | Upper limit (as a percentage of the service's `desired_count`) of the number of running tasks that can be running in a service during a deployment | `number` | `200` | no | | [deployment\_minimum\_healthy\_percent](#input\_deployment\_minimum\_healthy\_percent) | Lower limit (as a percentage of the service's `desired_count`) of the number of running tasks that must remain running and healthy in a service during a deployment | `number` | `66` | no | | [desired\_count](#input\_desired\_count) | Number of instances of the task definition to place and keep running | `number` | `1` | no | -| [disable\_default\_name\_postfix](#input\_disable\_default\_name\_postfix) | [DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names | `bool` | `false` | no | +| [disable\_v7\_default\_name\_description](#input\_disable\_v7\_default\_name\_description) | [DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names | `bool` | `false` | no | | [enable\_autoscaling](#input\_enable\_autoscaling) | Determines whether to enable autoscaling for the service | `bool` | `true` | no | | [enable\_ecs\_managed\_tags](#input\_enable\_ecs\_managed\_tags) | Specifies whether to enable Amazon ECS managed tags for the tasks within the service | `bool` | `true` | no | | [enable\_execute\_command](#input\_enable\_execute\_command) | Specifies whether to enable Amazon ECS Exec for the tasks within the service | `bool` | `false` | no | diff --git a/modules/service/main.tf b/modules/service/main.tf index cdcc9237..a10798f4 100644 --- a/modules/service/main.tf +++ b/modules/service/main.tf @@ -753,7 +753,7 @@ resource "aws_iam_role" "service" { name = var.iam_role_use_name_prefix ? null : local.iam_role_name name_prefix = var.iam_role_use_name_prefix ? "${local.iam_role_name}-" : null path = var.iam_role_path - description = var.iam_role_description + description = try(coalesce(var.iam_role_description, (var.disable_v7_default_name_description ? null : "IAM role for ECS Service ${var.name}")), null) assume_role_policy = data.aws_iam_policy_document.service_assume[0].json permissions_boundary = var.iam_role_permissions_boundary @@ -1059,7 +1059,7 @@ resource "aws_ecs_task_definition" "this" { ################################################################################ locals { - task_exec_iam_role_name = coalesce(var.task_exec_iam_role_name, "${var.name}${var.disable_default_name_postfix ? "" : "-task-exec"}") + task_exec_iam_role_name = coalesce(var.task_exec_iam_role_name, "${var.name}${var.disable_v7_default_name_description ? "" : "-task-exec"}") create_task_exec_iam_role = local.create_task_definition && var.create_task_exec_iam_role create_task_exec_policy = local.create_task_exec_iam_role && var.create_task_exec_policy @@ -1213,7 +1213,7 @@ resource "aws_iam_role_policy_attachment" "task_exec" { ################################################################################ locals { - tasks_iam_role_name = coalesce(var.tasks_iam_role_name, "${var.name}${var.disable_default_name_postfix ? "" : "-tasks"}") + tasks_iam_role_name = coalesce(var.tasks_iam_role_name, "${var.name}${var.disable_v7_default_name_description ? "" : "-tasks"}") create_tasks_iam_role = local.create_task_definition && var.create_tasks_iam_role } @@ -1250,7 +1250,7 @@ resource "aws_iam_role" "tasks" { name = var.tasks_iam_role_use_name_prefix ? null : local.tasks_iam_role_name name_prefix = var.tasks_iam_role_use_name_prefix ? "${local.tasks_iam_role_name}-" : null path = var.tasks_iam_role_path - description = var.tasks_iam_role_description + description = try(coalesce(var.tasks_iam_role_description, (var.disable_v7_default_name_description ? null : "IAM role for ECS tasks in Service ${var.name}")), null) assume_role_policy = data.aws_iam_policy_document.tasks_assume[0].json permissions_boundary = var.tasks_iam_role_permissions_boundary @@ -1870,7 +1870,7 @@ resource "aws_appautoscaling_scheduled_action" "this" { locals { create_security_group = var.create && var.create_security_group && var.network_mode == "awsvpc" - security_group_name = coalesce(var.security_group_name, "${var.name}${var.disable_default_name_postfix ? "" : "-service"}") + security_group_name = coalesce(var.security_group_name, "${var.name}${var.disable_v7_default_name_description ? "" : "-service"}") } data "aws_subnet" "this" { @@ -1888,7 +1888,7 @@ resource "aws_security_group" "this" { name = var.security_group_use_name_prefix ? null : local.security_group_name name_prefix = var.security_group_use_name_prefix ? "${local.security_group_name}-" : null - description = try(coalesce(var.security_group_description, (var.disable_default_name_postfix ? null : "Security group for ECS Service ${var.name}")), null) + description = try(coalesce(var.security_group_description, (var.disable_v7_default_name_description ? null : "Security group for ECS Service ${var.name}")), null) vpc_id = var.vpc_id != null ? var.vpc_id : data.aws_subnet.this[0].vpc_id tags = merge( @@ -1955,7 +1955,7 @@ locals { needs_infrastructure_iam_role = var.volume_configuration != null || var.vpc_lattice_configurations != null || var.deployment_configuration != null create_infrastructure_iam_role = var.create && var.create_infrastructure_iam_role && local.needs_infrastructure_iam_role infrastructure_iam_role_arn = local.needs_infrastructure_iam_role ? try(aws_iam_role.infrastructure_iam_role[0].arn, var.infrastructure_iam_role_arn) : null - infrastructure_iam_role_name = coalesce(var.infrastructure_iam_role_name, "${var.name}${var.disable_default_name_postfix ? "" : "-infra"}") + infrastructure_iam_role_name = coalesce(var.infrastructure_iam_role_name, "${var.name}${var.disable_v7_default_name_description ? "" : "-infra"}") } data "aws_iam_policy_document" "infrastructure_iam_role" { diff --git a/modules/service/variables.tf b/modules/service/variables.tf index a022a819..2f711ed8 100644 --- a/modules/service/variables.tf +++ b/modules/service/variables.tf @@ -25,7 +25,7 @@ variable "tags" { nullable = false } -variable "disable_default_name_postfix" { +variable "disable_v7_default_name_description" { description = "[DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names" type = bool default = false diff --git a/variables.tf b/variables.tf index ba03aa8b..76b6e4e7 100644 --- a/variables.tf +++ b/variables.tf @@ -16,8 +16,8 @@ variable "tags" { default = {} } -variable "disable_default_name_postfix" { - description = "[DEPRECATED - will be removed in next breaking change] Determines whether to disable the default postfix added to resource names" +variable "disable_v7_default_name_description" { + description = "[DEPRECATED - will be removed in v9.0] Determines whether to disable the default postfix added to resource names and descriptions added in v7.0" type = bool default = false } diff --git a/wrappers/cluster/main.tf b/wrappers/cluster/main.tf index 8e7d712a..40f33ef5 100644 --- a/wrappers/cluster/main.tf +++ b/wrappers/cluster/main.tf @@ -26,7 +26,7 @@ module "wrapper" { create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, false) create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) default_capacity_provider_strategy = try(each.value.default_capacity_provider_strategy, var.defaults.default_capacity_provider_strategy, {}) - disable_default_name_postfix = try(each.value.disable_default_name_postfix, var.defaults.disable_default_name_postfix, false) + disable_v7_default_name_description = try(each.value.disable_v7_default_name_description, var.defaults.disable_v7_default_name_description, false) infrastructure_iam_role_description = try(each.value.infrastructure_iam_role_description, var.defaults.infrastructure_iam_role_description, null) infrastructure_iam_role_name = try(each.value.infrastructure_iam_role_name, var.defaults.infrastructure_iam_role_name, null) infrastructure_iam_role_override_policy_documents = try(each.value.infrastructure_iam_role_override_policy_documents, var.defaults.infrastructure_iam_role_override_policy_documents, []) diff --git a/wrappers/main.tf b/wrappers/main.tf index 1cfc7a6b..5068926a 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -34,7 +34,7 @@ module "wrapper" { create_task_exec_iam_role = try(each.value.create_task_exec_iam_role, var.defaults.create_task_exec_iam_role, false) create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) default_capacity_provider_strategy = try(each.value.default_capacity_provider_strategy, var.defaults.default_capacity_provider_strategy, null) - disable_default_name_postfix = try(each.value.disable_default_name_postfix, var.defaults.disable_default_name_postfix, false) + disable_v7_default_name_description = try(each.value.disable_v7_default_name_description, var.defaults.disable_v7_default_name_description, false) infrastructure_iam_role_description = try(each.value.infrastructure_iam_role_description, var.defaults.infrastructure_iam_role_description, null) infrastructure_iam_role_name = try(each.value.infrastructure_iam_role_name, var.defaults.infrastructure_iam_role_name, null) infrastructure_iam_role_override_policy_documents = try(each.value.infrastructure_iam_role_override_policy_documents, var.defaults.infrastructure_iam_role_override_policy_documents, []) diff --git a/wrappers/service/main.tf b/wrappers/service/main.tf index 0202f07c..a8b2d841 100644 --- a/wrappers/service/main.tf +++ b/wrappers/service/main.tf @@ -49,7 +49,7 @@ module "wrapper" { deployment_maximum_percent = try(each.value.deployment_maximum_percent, var.defaults.deployment_maximum_percent, 200) deployment_minimum_healthy_percent = try(each.value.deployment_minimum_healthy_percent, var.defaults.deployment_minimum_healthy_percent, 66) desired_count = try(each.value.desired_count, var.defaults.desired_count, 1) - disable_default_name_postfix = try(each.value.disable_default_name_postfix, var.defaults.disable_default_name_postfix, false) + disable_v7_default_name_description = try(each.value.disable_v7_default_name_description, var.defaults.disable_v7_default_name_description, false) enable_autoscaling = try(each.value.enable_autoscaling, var.defaults.enable_autoscaling, true) enable_ecs_managed_tags = try(each.value.enable_ecs_managed_tags, var.defaults.enable_ecs_managed_tags, true) enable_execute_command = try(each.value.enable_execute_command, var.defaults.enable_execute_command, false) From 6a3f444ffeee93465b931da4e1ffec53e9c61a36 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Wed, 17 Dec 2025 13:14:34 -0600 Subject: [PATCH 10/10] chore: Update documentation add add upgrade guide --- README.md | 9 +- docs/UPGRADE-7.0.md | 211 +++++++++++++++++++++++++ examples/complete/main.tf | 2 +- examples/ec2-autoscaling/main.tf | 20 +-- examples/fargate/main.tf | 20 +-- examples/managed-instances/README.md | 1 + examples/managed-instances/main.tf | 8 +- examples/managed-instances/outputs.tf | 9 ++ modules/cluster/README.md | 123 +++++++++++--- modules/container-definition/README.md | 8 +- modules/service/README.md | 9 +- 11 files changed, 364 insertions(+), 56 deletions(-) create mode 100644 docs/UPGRADE-7.0.md diff --git a/README.md b/README.md index ce838770..2900c4c5 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ module "ecs" { } # Cluster capacity providers + cluster_capacity_providers = ["FARGATE", "FARGATE_SPOT"] default_capacity_provider_strategy = { FARGATE = { weight = 50 @@ -146,9 +147,11 @@ module "ecs" { ## Examples -- [ECS Cluster Complete](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/complete) -- [ECS Cluster w/ EC2 Autoscaling Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/ec2-autoscaling) -- [ECS Cluster w/ Fargate Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/fargate) +- [ECS cluster w/ integrated service(s)](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/complete) +- [ECS container definition](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/container-definition) +- [ECS cluster w/ EC2 Autoscaling capacity provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/ec2-autoscaling) +- [ECS cluster w/ Fargate capacity provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/fargate) +- [ECS cluster w/ ECS managed instances capacity provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/managed-instances) ## Requirements diff --git a/docs/UPGRADE-7.0.md b/docs/UPGRADE-7.0.md new file mode 100644 index 00000000..57be806a --- /dev/null +++ b/docs/UPGRADE-7.0.md @@ -0,0 +1,211 @@ +# Upgrade from v6.x to v7.x + +If you have any questions regarding this upgrade process, please consult the [`examples`](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples) directory: +If you find a bug, please open an issue with supporting configuration to reproduce. + +## List of backwards incompatible changes + +- Previously the module would infer the capacity providers to use based on those specified in the `default_capacity_provider_strategy` variable as well as any specified in the `autoscaling_capacity_providers` variable. As of v7.0.0, the module will no longer infer the capacity providers that should be associated with the cluster. Instead, users must explicitly specify the desired capacity providers using the new `cluster_capacity_providers` variable. The only inference of capacity providers are those created by the module itself when using the `capacity_providers` variable. Essentially, if you are using `FARGATE`, `FARGATE_SPOT`, or an externally created capacity provider, you must now specify those in the `cluster_capacity_providers` variable. +- With the addition of ECS managed instances support, the prior variable `autoscaling_capacity_providers` has been replaced with the more generic `capacity_providers` variable. If you were previously using `autoscaling_capacity_providers`, you will need to migrate to the new `capacity_providers` variable by simply renaming it and nesting each ASG capacity provider definition under the argument `auto_scaling_group_provider`. See the before vs after section below for an example of this change. Note: your existing ASG capacity providers will continue to work as before, this is simply a variable rename and variable definition modification. No resources will be replaced/destroyed as part of this change. +- The ECS service variable `ordered_placement_strategy` type definition has been changed from `map(object({...}))` to `list(object({...}))`. The argument needs to preserve order so a list is necessary. + +## Additional changes + +### Added + +- Default name postfixes for IAM roles and security groups have been added, along with default descriptions. When using the intended behavior of simply setting a `var.name` value and relying on the module, these new defaults help to distinguish resources created by the module. Instead of seeing 4 IAM roles named `"example-"`, you will now see names like `"example-service-"`, `"example-task-exec-"`, `"example-tasks-"`, and `"example-infra-"`. To aid in the migration, a variable `disable_v7_default_name_description` has been added that allow users to opt out of theses default settings for existing resources (avoid re-creating them). This ensures an easier upgrade path while also letting new resources benefit from the improved naming and descriptions. Note: this variable and therefore its behavior will be removed in version `v9.0` of the module, giving users time to remediate. +- Support for ECS managed instances has been added. Users can now create an ECS cluster that use EC2 instances created and managed by ECS managed instances capacity provider. Support for this includes the necessary IAM roles as well as a security group that is utilized by the managed instances. + +### Modified + +- The ECS service infrastructure IAM role is now associated with the `lifecycle_hook` and `advanced_configuration` arguments as part of the progressive deployment options. Users can still provide their own role, but the default now matches the rest of the module where the infrastructure IAM role created by the module will be used unless a different IAM role is provided. + +### Variable and output changes + +> [!NOTE] +> The variables and outputs added for ECS managed instance support has not been added to this list. Those details are not relevant to the upgrade process. See the [pull request](https://github.com/terraform-aws-modules/terraform-aws-ecs/pull/364) for more details on what has been added for ECS managed instances support (or consult the documentation/examples within the repository). + +1. Removed variables: + + - None + +2. Renamed variables: + + - `autoscaling_capacity_providers` -> `capacity_providers` + +3. Added variables: + + - `cluster_capacity_providers` + - `disable_v7_default_name_description` + +4. Removed outputs: + + - None + +5. Renamed outputs: + + - `autoscaling_capacity_providers` -> `capacity_providers` + +6. Added outputs: + + - None + +## Upgrade Migrations + +### Before 6.x Example + +#### Root Module + +```hcl +module "ecs" { + source = "terraform-aws-modules/ecs/aws" + version = "~> 6.0" + + # Truncated for brevity ... + + default_capacity_provider_strategy = { + FARGATE = { + weight = 50 + base = 20 + } + FARGATE_SPOT = { + weight = 50 + } + } + + autoscaling_capacity_providers = { + ASG = { + auto_scaling_group_arn = module.autoscaling.autoscaling_group_arn + managed_draining = "ENABLED" + managed_termination_protection = "ENABLED" + + managed_scaling = { + maximum_scaling_step_size = 5 + minimum_scaling_step_size = 1 + status = "ENABLED" + target_capacity = 60 + } + } + } +} +``` + +#### Cluster Sub-Module + +```hcl +module "ecs_cluster" { + source = "terraform-aws-modules/ecs/aws//modules/cluster" + version = "~> 6.0" + + # Truncated for brevity ... + + default_capacity_provider_strategy = { + FARGATE = { + weight = 50 + base = 20 + } + FARGATE_SPOT = { + weight = 50 + } + } + + autoscaling_capacity_providers = { + ASG = { + auto_scaling_group_arn = module.autoscaling.autoscaling_group_arn + managed_draining = "ENABLED" + managed_termination_protection = "ENABLED" + + managed_scaling = { + maximum_scaling_step_size = 5 + minimum_scaling_step_size = 1 + status = "ENABLED" + target_capacity = 60 + } + } + } +} +``` + +### After 7.x Example + +#### Root Module + +```hcl +module "ecs" { + source = "terraform-aws-modules/ecs/aws" + version = "~> 7.0" + + # Truncated for brevity ... + + cluster_capacity_providers = ["FARGATE", "FARGATE_SPOT"] # <=== add + default_capacity_provider_strategy = { + FARGATE = { + weight = 50 + base = 20 + } + FARGATE_SPOT = { + weight = 50 + } + } + + capacity_providers = { # <=== change name + ASG = { + auto_scaling_group_provider = { # <=== add + auto_scaling_group_arn = module.autoscaling.autoscaling_group_arn + managed_draining = "ENABLED" + managed_termination_protection = "ENABLED" + + managed_scaling = { + maximum_scaling_step_size = 5 + minimum_scaling_step_size = 1 + status = "ENABLED" + target_capacity = 60 + } + } # <=== add + } + } +} +``` + +#### Cluster Sub-Module + +```hcl +module "ecs_cluster" { + source = "terraform-aws-modules/ecs/aws//modules/cluster" + version = "~> 7.0" + + # Truncated for brevity ... + + cluster_capacity_providers = ["FARGATE", "FARGATE_SPOT"] # <=== add + default_capacity_provider_strategy = { + FARGATE = { + weight = 50 + base = 20 + } + FARGATE_SPOT = { + weight = 50 + } + } + + capacity_providers = { # <=== change name + ASG = { + auto_scaling_group_provider = { # <=== add + auto_scaling_group_arn = module.autoscaling.autoscaling_group_arn + managed_draining = "ENABLED" + managed_termination_protection = "ENABLED" + + managed_scaling = { + maximum_scaling_step_size = 5 + minimum_scaling_step_size = 1 + status = "ENABLED" + target_capacity = 60 + } + } # <=== add + } + } +} +``` + +### State Changes + +None required. diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 7bc8b48b..73858a6b 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -37,7 +37,7 @@ module "ecs" { cluster_name = local.name # Cluster capacity providers - cluster_capacity_providers = ["FARGATE", "FARGATE_SPOT", "ASG"] + cluster_capacity_providers = ["FARGATE", "FARGATE_SPOT"] default_capacity_provider_strategy = { FARGATE = { weight = 50 diff --git a/examples/ec2-autoscaling/main.tf b/examples/ec2-autoscaling/main.tf index 369fa1d6..5b978ed8 100644 --- a/examples/ec2-autoscaling/main.tf +++ b/examples/ec2-autoscaling/main.tf @@ -38,20 +38,20 @@ module "ecs_cluster" { # Cluster capacity providers default_capacity_provider_strategy = { - ex_1 = { + ex-1 = { weight = 60 base = 20 } - ex_2 = { + ex-2 = { weight = 40 } } capacity_providers = { # On-demand instances - ex_1 = { + ex-1 = { auto_scaling_group_provider = { - auto_scaling_group_arn = module.autoscaling["ex_1"].autoscaling_group_arn + auto_scaling_group_arn = module.autoscaling["ex-1"].autoscaling_group_arn managed_draining = "ENABLED" managed_termination_protection = "ENABLED" @@ -64,9 +64,9 @@ module "ecs_cluster" { } } # Spot instances - ex_2 = { + ex-2 = { auto_scaling_group_provider = { - auto_scaling_group_arn = module.autoscaling["ex_2"].autoscaling_group_arn + auto_scaling_group_arn = module.autoscaling["ex-2"].autoscaling_group_arn managed_draining = "ENABLED" managed_termination_protection = "ENABLED" @@ -123,8 +123,8 @@ module "ecs_service" { requires_compatibilities = ["EC2"] capacity_provider_strategy = { # On-demand instances - ex_1 = { - capacity_provider = module.ecs_cluster.capacity_providers["ex_1"].name + ex-1 = { + capacity_provider = module.ecs_cluster.capacity_providers["ex-1"].name weight = 1 base = 1 } @@ -337,7 +337,7 @@ module "autoscaling" { for_each = { # On-demand instances - ex_1 = { + ex-1 = { instance_type = "t3.large" use_mixed_instances_policy = false mixed_instances_policy = null @@ -353,7 +353,7 @@ module "autoscaling" { EOT } # Spot instances - ex_2 = { + ex-2 = { instance_type = "t3.medium" use_mixed_instances_policy = true mixed_instances_policy = { diff --git a/examples/fargate/main.tf b/examples/fargate/main.tf index ca74c5cd..386c529c 100644 --- a/examples/fargate/main.tf +++ b/examples/fargate/main.tf @@ -177,15 +177,15 @@ module "ecs_service" { load_balancer = { service = { - target_group_arn = module.alb.target_groups["ex_ecs"].arn + target_group_arn = module.alb.target_groups["ex-ecs"].arn container_name = local.container_name container_port = local.container_port # for blue/green deployments advanced_configuration = { - alternate_target_group_arn = module.alb.target_groups["ex_ecs_alternate"].arn - production_listener_rule = module.alb.listener_rules["ex_http/production"].arn - test_listener_rule = module.alb.listener_rules["ex_http/test"].arn + alternate_target_group_arn = module.alb.target_groups["ex-ecs-alternate"].arn + production_listener_rule = module.alb.listener_rules["ex-http/production"].arn + test_listener_rule = module.alb.listener_rules["ex-http/test"].arn } } } @@ -309,7 +309,7 @@ module "alb" { } listeners = { - ex_http = { + ex-http = { port = 80 protocol = "HTTP" @@ -328,11 +328,11 @@ module "alb" { weighted_forward = { target_groups = [ { - target_group_key = "ex_ecs" + target_group_key = "ex-ecs" weight = 100 }, { - target_group_key = "ex_ecs_alternate" + target_group_key = "ex-ecs-alternate" weight = 0 } ] @@ -354,7 +354,7 @@ module "alb" { weighted_forward = { target_groups = [ { - target_group_key = "ex_ecs_alternate" + target_group_key = "ex-ecs-alternate" weight = 100 } ] @@ -374,7 +374,7 @@ module "alb" { } target_groups = { - ex_ecs = { + ex-ecs = { backend_protocol = "HTTP" backend_port = local.container_port target_type = "ip" @@ -399,7 +399,7 @@ module "alb" { } # for blue/green deployments - ex_ecs_alternate = { + ex-ecs-alternate = { backend_protocol = "HTTP" backend_port = local.container_port target_type = "ip" diff --git a/examples/managed-instances/README.md b/examples/managed-instances/README.md index f2b4d079..71cabe58 100644 --- a/examples/managed-instances/README.md +++ b/examples/managed-instances/README.md @@ -72,6 +72,7 @@ No inputs. | Name | Description | |------|-------------| +| [alb\_dns\_name](#output\_alb\_dns\_name) | The DNS name of the load balancer | | [capacity\_providers](#output\_capacity\_providers) | Map of autoscaling capacity providers created and their attributes | | [cloudwatch\_log\_group\_arn](#output\_cloudwatch\_log\_group\_arn) | ARN of CloudWatch log group created | | [cloudwatch\_log\_group\_name](#output\_cloudwatch\_log\_group\_name) | Name of CloudWatch log group created | diff --git a/examples/managed-instances/main.tf b/examples/managed-instances/main.tf index 306885bc..37a5cd07 100644 --- a/examples/managed-instances/main.tf +++ b/examples/managed-instances/main.tf @@ -129,7 +129,7 @@ module "ecs_service" { load_balancer = { service = { - target_group_arn = module.alb.target_groups["ex_ecs"].arn + target_group_arn = module.alb.target_groups["ex-ecs"].arn container_name = local.container_name container_port = local.container_port } @@ -182,18 +182,18 @@ module "alb" { } listeners = { - ex_http = { + ex-http = { port = local.container_port protocol = "HTTP" forward = { - target_group_key = "ex_ecs" + target_group_key = "ex-ecs" } } } target_groups = { - ex_ecs = { + ex-ecs = { backend_protocol = "HTTP" backend_port = local.container_port target_type = "ip" diff --git a/examples/managed-instances/outputs.tf b/examples/managed-instances/outputs.tf index 43506223..af60e017 100644 --- a/examples/managed-instances/outputs.tf +++ b/examples/managed-instances/outputs.tf @@ -215,3 +215,12 @@ output "service_security_group_id" { description = "ID of the security group" value = module.ecs_service.security_group_id } + +################################################################################ +# Application Load Balancer +################################################################################ + +output "alb_dns_name" { + description = "The DNS name of the load balancer" + value = module.alb.dns_name +} diff --git a/modules/cluster/README.md b/modules/cluster/README.md index fd6bbd83..7c59fbe9 100644 --- a/modules/cluster/README.md +++ b/modules/cluster/README.md @@ -5,6 +5,7 @@ Terraform module which creates Amazon ECS (Elastic Container Service) cluster re ## Available Features - ECS cluster +- ECS managed instances capacity providers including necessary IAM roles, permissions, and security group - Fargate capacity providers - EC2 AutoScaling Group capacity providers - ECS Service w/ task definition, task set, and container definition support @@ -13,6 +14,77 @@ For more details see the [design doc](https://github.com/terraform-aws-modules/t ## Usage +### ECS Managed Instances Capacity Providers + +```hcl +module "ecs_cluster" { + source = "terraform-aws-modules/ecs/aws//modules/cluster" + + name = "ecs-managed-instances" + + configuration = { + execute_command_configuration = { + logging = "OVERRIDE" + log_configuration = { + cloud_watch_log_group_name = "/aws/ecs/aws-managed-instances" + } + } + } + + capacity_providers = { + mi-example = { + managed_instances_provider = { + instance_launch_template = { + instance_requirements = { + instance_generations = ["current"] + cpu_manufacturers = ["intel", "amd"] + + memory_mib = { + max = 8192 + min = 1024 + } + + vcpu_count = { + max = 4 + min = 1 + } + } + + network_configuration = { + subnets = ["subnet-abcde012", "subnet-bcde012a", "subnet-fghi345a"] + } + + storage_configuration = { + storage_size_gib = 30 + } + } + } + } + } + + # Managed instances security group + vpc_id = "vpc-1234556abcdef" + security_group_ingress_rules = { + alb-http = { + from_port = 4000 + description = "Service port" + referenced_security_group_id = "sg-12345678" + } + } + security_group_egress_rules = { + all = { + cidr_ipv4 = "0.0.0.0/0" + ip_protocol = "-1" + } + } + + tags = { + Environment = "Development" + Project = "EcsEc2" + } +} +``` + ### Fargate Capacity Providers ```hcl @@ -30,6 +102,7 @@ module "ecs_cluster" { } } + cluster_capacity_providers = ["FARGATE", "FARGATE_SPOT"] default_capacity_provider_strategy = { FARGATE = { weight = 50 @@ -74,29 +147,33 @@ module "ecs_cluster" { } } - autoscaling_capacity_providers = { + capacity_providers = { one = { - auto_scaling_group_arn = "arn:aws:autoscaling:eu-west-1:012345678901:autoScalingGroup:08419a61:autoScalingGroupName/ecs-ec2-one-20220603194933774300000011" - managed_draining = "DISABLED" - managed_termination_protection = "ENABLED" - - managed_scaling = { - maximum_scaling_step_size = 5 - minimum_scaling_step_size = 1 - status = "ENABLED" - target_capacity = 60 + auto_scaling_group_provider = { + auto_scaling_group_arn = "arn:aws:autoscaling:eu-west-1:012345678901:autoScalingGroup:08419a61:autoScalingGroupName/ecs-ec2-one-20220603194933774300000011" + managed_draining = "DISABLED" + managed_termination_protection = "ENABLED" + + managed_scaling = { + maximum_scaling_step_size = 5 + minimum_scaling_step_size = 1 + status = "ENABLED" + target_capacity = 60 + } } } two = { - auto_scaling_group_arn = "arn:aws:autoscaling:eu-west-1:012345678901:autoScalingGroup:08419a61:autoScalingGroupName/ecs-ec2-two-20220603194933774300000022" - managed_draining = "ENABLED" - managed_termination_protection = "ENABLED" - - managed_scaling = { - maximum_scaling_step_size = 15 - minimum_scaling_step_size = 5 - status = "ENABLED" - target_capacity = 90 + auto_scaling_group_provider = { + auto_scaling_group_arn = "arn:aws:autoscaling:eu-west-1:012345678901:autoScalingGroup:08419a61:autoScalingGroupName/ecs-ec2-two-20220603194933774300000022" + managed_draining = "ENABLED" + managed_termination_protection = "ENABLED" + + managed_scaling = { + maximum_scaling_step_size = 15 + minimum_scaling_step_size = 5 + status = "ENABLED" + target_capacity = 90 + } } } } @@ -125,9 +202,11 @@ module "ecs_cluster" { ## Examples -- [ECS Cluster Complete](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/complete) -- [ECS Cluster w/ EC2 Autoscaling Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/ec2-autoscaling) -- [ECS Cluster w/ Fargate Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/fargate) +- [ECS cluster w/ integrated service(s)](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/complete) +- [ECS container definition](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/container-definition) +- [ECS cluster w/ EC2 Autoscaling capacity provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/ec2-autoscaling) +- [ECS cluster w/ Fargate capacity provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/fargate) +- [ECS cluster w/ ECS managed instances capacity provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/managed-instances) ## Requirements diff --git a/modules/container-definition/README.md b/modules/container-definition/README.md index 35e7a655..acbbc571 100644 --- a/modules/container-definition/README.md +++ b/modules/container-definition/README.md @@ -106,9 +106,11 @@ module "example_ecs_container_definition" { ## Examples -- [ECS Cluster Complete](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/complete) -- [ECS Cluster w/ EC2 Autoscaling Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/ec2-autoscaling) -- [ECS Cluster w/ Fargate Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/fargate) +- [ECS cluster w/ integrated service(s)](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/complete) +- [ECS container definition](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/container-definition) +- [ECS cluster w/ EC2 Autoscaling capacity provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/ec2-autoscaling) +- [ECS cluster w/ Fargate capacity provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/fargate) +- [ECS cluster w/ ECS managed instances capacity provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/managed-instances) ## Requirements diff --git a/modules/service/README.md b/modules/service/README.md index 6152ce48..c4ecd038 100644 --- a/modules/service/README.md +++ b/modules/service/README.md @@ -3,6 +3,7 @@ Configuration in this directory creates an Amazon ECS Service and associated resources. Some notable configurations to be aware of when using this module: + 1. `desired_count`/`scale` is always ignored; the module is designed to utilize autoscaling by default (though it can be disabled) 2. The default configuration is intended for `FARGATE` use @@ -160,9 +161,11 @@ module "ecs_service" { ## Examples -- [ECS Cluster Complete](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/complete) -- [ECS Cluster w/ EC2 Autoscaling Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/ec2-autoscaling) -- [ECS Cluster w/ Fargate Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/fargate) +- [ECS cluster w/ integrated service(s)](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/complete) +- [ECS container definition](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/container-definition) +- [ECS cluster w/ EC2 Autoscaling capacity provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/ec2-autoscaling) +- [ECS cluster w/ Fargate capacity provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/fargate) +- [ECS cluster w/ ECS managed instances capacity provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/managed-instances) ## Requirements