From 2778c5e8b54f7459484e2ca941e9b6988be95116 Mon Sep 17 00:00:00 2001 From: magreenbaum Date: Sat, 19 Jul 2025 21:34:58 -0400 Subject: [PATCH 1/6] blue/green deployments --- README.md | 4 +- examples/complete/README.md | 4 +- 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/versions.tf | 2 +- examples/fargate/README.md | 4 +- examples/fargate/versions.tf | 2 +- main.tf | 1 + modules/cluster/README.md | 4 +- modules/cluster/versions.tf | 2 +- modules/container-definition/README.md | 4 +- modules/container-definition/versions.tf | 2 +- modules/service/README.md | 9 +- modules/service/main.tf | 104 ++++++++++++++++++++++ modules/service/variables.tf | 28 ++++++ modules/service/versions.tf | 2 +- variables.tf | 23 +++++ versions.tf | 2 +- wrappers/cluster/versions.tf | 2 +- wrappers/container-definition/versions.tf | 2 +- wrappers/service/main.tf | 1 + wrappers/service/versions.tf | 2 +- wrappers/versions.tf | 2 +- 25 files changed, 187 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 5e1ba9e..09f710d 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ module "ecs" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.5.7 | -| [aws](#requirement\_aws) | >= 6.0 | +| [aws](#requirement\_aws) | >= 6.4 | ## Providers @@ -193,7 +193,7 @@ No resources. | [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 | | [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, true)
create_service = optional(bool, true)
tags = optional(map(string), {})

# Service
ignore_task_definition_changes = optional(bool, false)
alarms = optional(object({
alarm_names = list(string)
enable = optional(bool, true)
rollback = optional(bool, true)
}))
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_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, true)
enable_execute_command = optional(bool, false)
force_delete = optional(bool)
force_new_deployment = optional(bool, true)
health_check_grace_period_seconds = optional(number)
launch_type = optional(string, "FARGATE")
load_balancer = optional(map(object({
container_name = string
container_port = number
elb_name = optional(string)
target_group_arn = optional(string)
})))
name = optional(string) # Will fall back to use map key if not set
assign_public_ip = optional(bool, false)
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, true)
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
}))
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
}))
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, "TASK_DEFINITION")
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, true)
iam_role_arn = optional(string)
iam_role_name = optional(string)
iam_role_use_name_prefix = optional(bool, true)
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, true)
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)
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, true)
create_cloudwatch_log_group = optional(bool, true)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool, false)
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, "awsvpc")
pid_mode = optional(string)
proxy_configuration = optional(object({
container_name = string
properties = optional(map(string))
type = optional(string)
}))
requires_compatibilities = optional(list(string), ["FARGATE"])
runtime_platform = optional(object({
cpu_architecture = optional(string, "X86_64")
operating_system_family = optional(string, "LINUX")
}),
# Default
{
cpu_architecture = "X86_64"
operating_system_family = "LINUX"
}
)
skip_destroy = optional(bool)
task_definition_placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
track_latest = optional(bool, true)
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, true)
task_exec_iam_role_arn = optional(string)
task_exec_iam_role_name = optional(string)
task_exec_iam_role_use_name_prefix = optional(bool, true)
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, true)
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, true)
tasks_iam_role_arn = optional(string)
tasks_iam_role_name = optional(string)
tasks_iam_role_use_name_prefix = optional(bool, true)
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, true)
autoscaling_min_capacity = optional(number, 1)
autoscaling_max_capacity = optional(number, 10)
autoscaling_policies = optional(map(object({
name = optional(string) # Will fall back to the key name if not provided
policy_type = optional(string, "TargetTrackingScaling")
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, 300)
scale_out_cooldown = optional(number, 60)
target_value = optional(number, 75)
}))
})),
# Default
{
cpu = {
policy_type = "TargetTrackingScaling"

target_tracking_scaling_policy_configuration = {
predefined_metric_specification = {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
}
}
memory = {
policy_type = "TargetTrackingScaling"

target_tracking_scaling_policy_configuration = {
predefined_metric_specification = {
predefined_metric_type = "ECSServiceAverageMemoryUtilization"
}
}
}
}
)
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)
})))
# Security Group
create_security_group = optional(bool, true)
security_group_name = optional(string)
security_group_use_name_prefix = optional(bool, true)
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, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
})),
# Default
{}
)
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, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
})),
# Default
{}
)
security_group_tags = optional(map(string), {})
# ECS Infrastructure IAM Role
create_infrastructure_iam_role = optional(bool, true)
infrastructure_iam_role_arn = optional(string)
infrastructure_iam_role_name = optional(string)
infrastructure_iam_role_use_name_prefix = optional(bool, true)
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, true)
create_service = optional(bool, true)
tags = optional(map(string), {})

# Service
ignore_task_definition_changes = optional(bool, false)
alarms = optional(object({
alarm_names = list(string)
enable = optional(bool, true)
rollback = optional(bool, true)
}))
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)
lifecycle_hook = optional(object({
hook_target_arn = string
role_arn = string
lifecycle_stages = 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, true)
enable_execute_command = optional(bool, false)
force_delete = optional(bool)
force_new_deployment = optional(bool, true)
health_check_grace_period_seconds = optional(number)
launch_type = optional(string, "FARGATE")
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, false)
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, true)
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(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
}))
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, "TASK_DEFINITION")
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, true)
iam_role_arn = optional(string)
iam_role_name = optional(string)
iam_role_use_name_prefix = optional(bool, true)
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, true)
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)
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, true)
create_cloudwatch_log_group = optional(bool, true)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool, false)
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, "awsvpc")
pid_mode = optional(string)
proxy_configuration = optional(object({
container_name = string
properties = optional(map(string))
type = optional(string)
}))
requires_compatibilities = optional(list(string), ["FARGATE"])
runtime_platform = optional(object({
cpu_architecture = optional(string, "X86_64")
operating_system_family = optional(string, "LINUX")
}),
# Default
{
cpu_architecture = "X86_64"
operating_system_family = "LINUX"
}
)
skip_destroy = optional(bool)
task_definition_placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
track_latest = optional(bool, true)
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, true)
task_exec_iam_role_arn = optional(string)
task_exec_iam_role_name = optional(string)
task_exec_iam_role_use_name_prefix = optional(bool, true)
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, true)
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, true)
tasks_iam_role_arn = optional(string)
tasks_iam_role_name = optional(string)
tasks_iam_role_use_name_prefix = optional(bool, true)
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, true)
autoscaling_min_capacity = optional(number, 1)
autoscaling_max_capacity = optional(number, 10)
autoscaling_policies = optional(map(object({
name = optional(string) # Will fall back to the key name if not provided
policy_type = optional(string, "TargetTrackingScaling")
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, 300)
scale_out_cooldown = optional(number, 60)
target_value = optional(number, 75)
}))
})),
# Default
{
cpu = {
policy_type = "TargetTrackingScaling"

target_tracking_scaling_policy_configuration = {
predefined_metric_specification = {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
}
}
memory = {
policy_type = "TargetTrackingScaling"

target_tracking_scaling_policy_configuration = {
predefined_metric_specification = {
predefined_metric_type = "ECSServiceAverageMemoryUtilization"
}
}
}
}
)
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)
})))
# Security Group
create_security_group = optional(bool, true)
security_group_name = optional(string)
security_group_use_name_prefix = optional(bool, true)
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, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
})),
# Default
{}
)
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, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
})),
# Default
{}
)
security_group_tags = optional(map(string), {})
# ECS Infrastructure IAM Role
create_infrastructure_iam_role = optional(bool, true)
infrastructure_iam_role_arn = optional(string)
infrastructure_iam_role_name = optional(string)
infrastructure_iam_role_use_name_prefix = optional(bool, true)
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/README.md b/examples/complete/README.md index 090e1c3..817eb4e 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.0 | +| [aws](#requirement\_aws) | >= 6.4 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 6.0 | +| [aws](#provider\_aws) | >= 6.4 | ## Modules diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index db13b0a..497e3e6 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.0" + version = ">= 6.4" } } } diff --git a/examples/container-definition/README.md b/examples/container-definition/README.md index 46b7bd3..2a5ab63 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.0 | +| [aws](#requirement\_aws) | >= 6.4 | | [local](#requirement\_local) | >= 2.5 | ## Providers diff --git a/examples/container-definition/versions.tf b/examples/container-definition/versions.tf index f5b245b..9efd186 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.0" + version = ">= 6.4" } local = { source = "hashicorp/local" diff --git a/examples/ec2-autoscaling/README.md b/examples/ec2-autoscaling/README.md index c08d6c1..fd50be0 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.0 | +| [aws](#requirement\_aws) | >= 6.4 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 6.0 | +| [aws](#provider\_aws) | >= 6.4 | ## Modules diff --git a/examples/ec2-autoscaling/versions.tf b/examples/ec2-autoscaling/versions.tf index db13b0a..497e3e6 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.0" + version = ">= 6.4" } } } diff --git a/examples/fargate/README.md b/examples/fargate/README.md index 440a45c..0b6ef0f 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.0 | +| [aws](#requirement\_aws) | >= 6.4 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 6.0 | +| [aws](#provider\_aws) | >= 6.4 | ## Modules diff --git a/examples/fargate/versions.tf b/examples/fargate/versions.tf index db13b0a..497e3e6 100644 --- a/examples/fargate/versions.tf +++ b/examples/fargate/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.0" + version = ">= 6.4" } } } diff --git a/main.tf b/main.tf index d2380a5..5e1d1fe 100644 --- a/main.tf +++ b/main.tf @@ -65,6 +65,7 @@ module "service" { capacity_provider_strategy = each.value.capacity_provider_strategy cluster_arn = module.cluster.arn deployment_circuit_breaker = each.value.deployment_circuit_breaker + deployment_configuration = each.value.deployment_configuration deployment_controller = each.value.deployment_controller deployment_maximum_percent = each.value.deployment_maximum_percent deployment_minimum_healthy_percent = each.value.deployment_minimum_healthy_percent diff --git a/modules/cluster/README.md b/modules/cluster/README.md index c44adee..06f4bd8 100644 --- a/modules/cluster/README.md +++ b/modules/cluster/README.md @@ -135,13 +135,13 @@ module "ecs_cluster" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.5.7 | -| [aws](#requirement\_aws) | >= 6.0 | +| [aws](#requirement\_aws) | >= 6.4 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 6.0 | +| [aws](#provider\_aws) | >= 6.4 | ## Modules diff --git a/modules/cluster/versions.tf b/modules/cluster/versions.tf index db13b0a..497e3e6 100644 --- a/modules/cluster/versions.tf +++ b/modules/cluster/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.0" + version = ">= 6.4" } } } diff --git a/modules/container-definition/README.md b/modules/container-definition/README.md index 2fc6448..665c1c4 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.0 | +| [aws](#requirement\_aws) | >= 6.4 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 6.0 | +| [aws](#provider\_aws) | >= 6.4 | ## Modules diff --git a/modules/container-definition/versions.tf b/modules/container-definition/versions.tf index db13b0a..497e3e6 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.0" + version = ">= 6.4" } } } diff --git a/modules/service/README.md b/modules/service/README.md index 70f4528..8e60736 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.0 | +| [aws](#requirement\_aws) | >= 6.4 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 6.0 | +| [aws](#provider\_aws) | >= 6.4 | ## Modules @@ -250,6 +250,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)
lifecycle_hook = optional(object({
hook_target_arn = string
role_arn = string
lifecycle_stages = 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 | @@ -282,7 +283,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)
}))
| `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
role_arn = 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 | @@ -304,7 +305,7 @@ module "ecs_service" { | [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\_configuration](#input\_service\_connect\_configuration) | The ECS Service Connect configuration for this service to discover and connect to services, and be discovered by, and connected from, other services within a namespace |
object({
enabled = optional(bool, true)
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
}))
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)
}))
})))
})
| `null` | no | +| [service\_connect\_configuration](#input\_service\_connect\_configuration) | The ECS Service Connect configuration for this service to discover and connect to services, and be discovered by, and connected from, other services within a namespace |
object({
enabled = optional(bool, true)
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(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)
}))
})))
})
| `null` | no | | [service\_registries](#input\_service\_registries) | Service discovery registries for the service |
object({
container_name = optional(string)
container_port = optional(number)
port = optional(number)
registry_arn = string
})
| `null` | no | | [service\_tags](#input\_service\_tags) | A map of additional tags to add to the service | `map(string)` | `{}` | no | | [skip\_destroy](#input\_skip\_destroy) | If true, the task is not deleted when the service is deleted | `bool` | `null` | no | diff --git a/modules/service/main.tf b/modules/service/main.tf index b3fc157..072bf28 100644 --- a/modules/service/main.tf +++ b/modules/service/main.tf @@ -73,6 +73,25 @@ resource "aws_ecs_service" "this" { } } + dynamic "deployment_configuration" { + for_each = var.deployment_configuration != null ? [var.deployment_configuration] : [] + + content { + strategy = try(deployment_configuration.value.strategy, null) + bake_time_in_minutes = try(deployment_configuration.value.bake_time_in_minutes, null) + + dynamic "lifecycle_hook" { + for_each = deployment_configuration.value.lifecycle_hook != null ? [deployment_configuration.value.lifecycle_hook] : [] + + content { + hook_target_arn = lifecycle_hook.value.hook_target_arn + role_arn = lifecycle_hook.value.role_arn + lifecycle_stages = lifecycle_hook.value.lifecycle_stages + } + } + } + } + dynamic "deployment_controller" { for_each = var.deployment_controller != null ? [var.deployment_controller] : [] @@ -101,6 +120,17 @@ resource "aws_ecs_service" "this" { container_port = load_balancer.value.container_port elb_name = load_balancer.value.elb_name target_group_arn = load_balancer.value.target_group_arn + + dynamic "advanced_configuration" { + for_each = load_balancer.value.advanced_configuration != null ? [load_balancer.value.advanced_configuration] : [] + + content { + alternate_target_group_arn = advanced_configuration.value.alternate_target_group_arn + production_listener_rule = advanced_configuration.value.production_listener_rule + role_arn = advanced_configuration.value.role_arn + test_listener_rule = try(advanced_configuration.value.test_listener_rule, null) + } + } } } @@ -176,6 +206,28 @@ resource "aws_ecs_service" "this" { content { dns_name = client_alias.value.dns_name port = client_alias.value.port + + dynamic "test_traffic_rules" { + for_each = client_alias.value.test_traffic_rules != null ? [client_alias.value.test_traffic_rules] : [] + + content { + dynamic "header" { + for_each = test_traffic_rules.value.header != null ? [test_traffic_rules.value.header] : [] + + content { + name = header.value.name + + dynamic "value" { + for_each = header.value.value != null ? [header.value.value] : [] + + content { + exact = value.value.exact + } + } + } + } + } + } } } @@ -341,6 +393,25 @@ resource "aws_ecs_service" "ignore_task_definition" { } } + dynamic "deployment_configuration" { + for_each = var.deployment_configuration != null ? [var.deployment_configuration] : [] + + content { + strategy = try(deployment_configuration.value.strategy, null) + bake_time_in_minutes = try(deployment_configuration.value.bake_time_in_minutes, null) + + dynamic "lifecycle_hook" { + for_each = deployment_configuration.value.lifecycle_hook != null ? [deployment_configuration.value.lifecycle_hook] : [] + + content { + hook_target_arn = lifecycle_hook.value.hook_target_arn + role_arn = lifecycle_hook.value.role_arn + lifecycle_stages = lifecycle_hook.value.lifecycle_stages + } + } + } + } + dynamic "deployment_controller" { for_each = var.deployment_controller != null ? [var.deployment_controller] : [] @@ -369,6 +440,17 @@ resource "aws_ecs_service" "ignore_task_definition" { container_port = load_balancer.value.container_port elb_name = load_balancer.value.elb_name target_group_arn = load_balancer.value.target_group_arn + + dynamic "advanced_configuration" { + for_each = load_balancer.value.advanced_configuration != null ? [load_balancer.value.advanced_configuration] : [] + + content { + alternate_target_group_arn = advanced_configuration.value.alternate_target_group_arn + production_listener_rule = advanced_configuration.value.production_listener_rule + role_arn = advanced_configuration.value.role_arn + test_listener_rule = try(advanced_configuration.value.test_listener_rule, null) + } + } } } @@ -444,6 +526,28 @@ resource "aws_ecs_service" "ignore_task_definition" { content { dns_name = client_alias.value.dns_name port = client_alias.value.port + + dynamic "test_traffic_rules" { + for_each = client_alias.value.test_traffic_rules != null ? [client_alias.value.test_traffic_rules] : [] + + content { + dynamic "header" { + for_each = test_traffic_rules.value.header != null ? [test_traffic_rules.value.header] : [] + + content { + name = header.value.name + + dynamic "value" { + for_each = header.value.value != null ? [header.value.value] : [] + + content { + exact = value.value.exact + } + } + } + } + } + } } } diff --git a/modules/service/variables.tf b/modules/service/variables.tf index f007632..f5e8bca 100644 --- a/modules/service/variables.tf +++ b/modules/service/variables.tf @@ -73,6 +73,20 @@ variable "deployment_circuit_breaker" { default = null } +variable "deployment_configuration" { + description = "Configuration block for deployment settings" + type = object({ + strategy = optional(string) + bake_time_in_minutes = optional(string) + lifecycle_hook = optional(object({ + hook_target_arn = string + role_arn = string + lifecycle_stages = string + })) + }) + default = null +} + variable "deployment_controller" { description = "Configuration block for deployment controller configuration" type = object({ @@ -142,6 +156,12 @@ variable "load_balancer" { 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) + })) })) default = null } @@ -223,6 +243,14 @@ variable "service_connect_configuration" { client_alias = optional(object({ dns_name = optional(string) port = number + test_traffic_rules = optional(object({ + header = optional(object({ + name = string + value = object({ + exact = string + }) + })) + })) })) discovery_name = optional(string) ingress_port_override = optional(number) diff --git a/modules/service/versions.tf b/modules/service/versions.tf index db13b0a..497e3e6 100644 --- a/modules/service/versions.tf +++ b/modules/service/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.0" + version = ">= 6.4" } } } diff --git a/variables.tf b/variables.tf index 6122006..cb0a9ec 100644 --- a/variables.tf +++ b/variables.tf @@ -280,6 +280,15 @@ variable "services" { enable = bool rollback = bool })) + deployment_configuration = optional(object({ + strategy = optional(string) + bake_time_in_minutes = optional(string) + lifecycle_hook = optional(object({ + hook_target_arn = string + role_arn = string + lifecycle_stages = string + })) + })) deployment_controller = optional(object({ type = optional(string) })) @@ -297,6 +306,12 @@ variable "services" { 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, false) @@ -328,6 +343,14 @@ variable "services" { client_alias = optional(object({ dns_name = optional(string) port = number + test_traffic_rules = optional(object({ + header = optional(object({ + name = string + value = object({ + exact = string + }) + })) + })) })) discovery_name = optional(string) ingress_port_override = optional(number) diff --git a/versions.tf b/versions.tf index db13b0a..497e3e6 100644 --- a/versions.tf +++ b/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.0" + version = ">= 6.4" } } } diff --git a/wrappers/cluster/versions.tf b/wrappers/cluster/versions.tf index db13b0a..497e3e6 100644 --- a/wrappers/cluster/versions.tf +++ b/wrappers/cluster/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.0" + version = ">= 6.4" } } } diff --git a/wrappers/container-definition/versions.tf b/wrappers/container-definition/versions.tf index db13b0a..497e3e6 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.0" + version = ">= 6.4" } } } diff --git a/wrappers/service/main.tf b/wrappers/service/main.tf index 96bdbbe..8cb3d9d 100644 --- a/wrappers/service/main.tf +++ b/wrappers/service/main.tf @@ -43,6 +43,7 @@ module "wrapper" { create_task_exec_policy = try(each.value.create_task_exec_policy, var.defaults.create_task_exec_policy, true) create_tasks_iam_role = try(each.value.create_tasks_iam_role, var.defaults.create_tasks_iam_role, true) deployment_circuit_breaker = try(each.value.deployment_circuit_breaker, var.defaults.deployment_circuit_breaker, null) + deployment_configuration = try(each.value.deployment_configuration, var.defaults.deployment_configuration, null) deployment_controller = try(each.value.deployment_controller, var.defaults.deployment_controller, null) 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) diff --git a/wrappers/service/versions.tf b/wrappers/service/versions.tf index db13b0a..497e3e6 100644 --- a/wrappers/service/versions.tf +++ b/wrappers/service/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.0" + version = ">= 6.4" } } } diff --git a/wrappers/versions.tf b/wrappers/versions.tf index db13b0a..497e3e6 100644 --- a/wrappers/versions.tf +++ b/wrappers/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.0" + version = ">= 6.4" } } } From 0fde8647d3bb3e1ed2d35e252c04866850a248df Mon Sep 17 00:00:00 2001 From: magreenbaum Date: Sun, 20 Jul 2025 11:50:55 -0400 Subject: [PATCH 2/6] updates --- README.md | 2 +- modules/service/README.md | 4 ++-- modules/service/main.tf | 8 ++++---- modules/service/variables.tf | 10 +++++----- variables.tf | 10 +++++----- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 09f710d..394d4c1 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ No resources. | [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 | | [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, true)
create_service = optional(bool, true)
tags = optional(map(string), {})

# Service
ignore_task_definition_changes = optional(bool, false)
alarms = optional(object({
alarm_names = list(string)
enable = optional(bool, true)
rollback = optional(bool, true)
}))
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)
lifecycle_hook = optional(object({
hook_target_arn = string
role_arn = string
lifecycle_stages = 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, true)
enable_execute_command = optional(bool, false)
force_delete = optional(bool)
force_new_deployment = optional(bool, true)
health_check_grace_period_seconds = optional(number)
launch_type = optional(string, "FARGATE")
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, false)
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, true)
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(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
}))
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, "TASK_DEFINITION")
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, true)
iam_role_arn = optional(string)
iam_role_name = optional(string)
iam_role_use_name_prefix = optional(bool, true)
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, true)
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)
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, true)
create_cloudwatch_log_group = optional(bool, true)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool, false)
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, "awsvpc")
pid_mode = optional(string)
proxy_configuration = optional(object({
container_name = string
properties = optional(map(string))
type = optional(string)
}))
requires_compatibilities = optional(list(string), ["FARGATE"])
runtime_platform = optional(object({
cpu_architecture = optional(string, "X86_64")
operating_system_family = optional(string, "LINUX")
}),
# Default
{
cpu_architecture = "X86_64"
operating_system_family = "LINUX"
}
)
skip_destroy = optional(bool)
task_definition_placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
track_latest = optional(bool, true)
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, true)
task_exec_iam_role_arn = optional(string)
task_exec_iam_role_name = optional(string)
task_exec_iam_role_use_name_prefix = optional(bool, true)
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, true)
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, true)
tasks_iam_role_arn = optional(string)
tasks_iam_role_name = optional(string)
tasks_iam_role_use_name_prefix = optional(bool, true)
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, true)
autoscaling_min_capacity = optional(number, 1)
autoscaling_max_capacity = optional(number, 10)
autoscaling_policies = optional(map(object({
name = optional(string) # Will fall back to the key name if not provided
policy_type = optional(string, "TargetTrackingScaling")
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, 300)
scale_out_cooldown = optional(number, 60)
target_value = optional(number, 75)
}))
})),
# Default
{
cpu = {
policy_type = "TargetTrackingScaling"

target_tracking_scaling_policy_configuration = {
predefined_metric_specification = {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
}
}
memory = {
policy_type = "TargetTrackingScaling"

target_tracking_scaling_policy_configuration = {
predefined_metric_specification = {
predefined_metric_type = "ECSServiceAverageMemoryUtilization"
}
}
}
}
)
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)
})))
# Security Group
create_security_group = optional(bool, true)
security_group_name = optional(string)
security_group_use_name_prefix = optional(bool, true)
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, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
})),
# Default
{}
)
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, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
})),
# Default
{}
)
security_group_tags = optional(map(string), {})
# ECS Infrastructure IAM Role
create_infrastructure_iam_role = optional(bool, true)
infrastructure_iam_role_arn = optional(string)
infrastructure_iam_role_name = optional(string)
infrastructure_iam_role_use_name_prefix = optional(bool, true)
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, true)
create_service = optional(bool, true)
tags = optional(map(string), {})

# Service
ignore_task_definition_changes = optional(bool, false)
alarms = optional(object({
alarm_names = list(string)
enable = optional(bool, true)
rollback = optional(bool, true)
}))
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)
lifecycle_hook = optional(map(object({
hook_target_arn = string
role_arn = string
lifecycle_stages = list(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, true)
enable_execute_command = optional(bool, false)
force_delete = optional(bool)
force_new_deployment = optional(bool, true)
health_check_grace_period_seconds = optional(number)
launch_type = optional(string, "FARGATE")
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, false)
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, true)
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
}))
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, "TASK_DEFINITION")
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, true)
iam_role_arn = optional(string)
iam_role_name = optional(string)
iam_role_use_name_prefix = optional(bool, true)
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, true)
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)
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, true)
create_cloudwatch_log_group = optional(bool, true)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool, false)
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, "awsvpc")
pid_mode = optional(string)
proxy_configuration = optional(object({
container_name = string
properties = optional(map(string))
type = optional(string)
}))
requires_compatibilities = optional(list(string), ["FARGATE"])
runtime_platform = optional(object({
cpu_architecture = optional(string, "X86_64")
operating_system_family = optional(string, "LINUX")
}),
# Default
{
cpu_architecture = "X86_64"
operating_system_family = "LINUX"
}
)
skip_destroy = optional(bool)
task_definition_placement_constraints = optional(map(object({
expression = optional(string)
type = string
})))
track_latest = optional(bool, true)
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, true)
task_exec_iam_role_arn = optional(string)
task_exec_iam_role_name = optional(string)
task_exec_iam_role_use_name_prefix = optional(bool, true)
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, true)
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, true)
tasks_iam_role_arn = optional(string)
tasks_iam_role_name = optional(string)
tasks_iam_role_use_name_prefix = optional(bool, true)
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, true)
autoscaling_min_capacity = optional(number, 1)
autoscaling_max_capacity = optional(number, 10)
autoscaling_policies = optional(map(object({
name = optional(string) # Will fall back to the key name if not provided
policy_type = optional(string, "TargetTrackingScaling")
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, 300)
scale_out_cooldown = optional(number, 60)
target_value = optional(number, 75)
}))
})),
# Default
{
cpu = {
policy_type = "TargetTrackingScaling"

target_tracking_scaling_policy_configuration = {
predefined_metric_specification = {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
}
}
memory = {
policy_type = "TargetTrackingScaling"

target_tracking_scaling_policy_configuration = {
predefined_metric_specification = {
predefined_metric_type = "ECSServiceAverageMemoryUtilization"
}
}
}
}
)
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)
})))
# Security Group
create_security_group = optional(bool, true)
security_group_name = optional(string)
security_group_use_name_prefix = optional(bool, true)
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, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
})),
# Default
{}
)
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, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
})),
# Default
{}
)
security_group_tags = optional(map(string), {})
# ECS Infrastructure IAM Role
create_infrastructure_iam_role = optional(bool, true)
infrastructure_iam_role_arn = optional(string)
infrastructure_iam_role_name = optional(string)
infrastructure_iam_role_use_name_prefix = optional(bool, true)
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 8e60736..babf5fc 100644 --- a/modules/service/README.md +++ b/modules/service/README.md @@ -250,7 +250,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)
lifecycle_hook = optional(object({
hook_target_arn = string
role_arn = string
lifecycle_stages = string
}))
})
| `null` | no | +| [deployment\_configuration](#input\_deployment\_configuration) | Configuration block for deployment settings |
object({
strategy = optional(string)
bake_time_in_minutes = optional(string)
lifecycle_hook = optional(map(object({
hook_target_arn = string
role_arn = string
lifecycle_stages = list(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 | @@ -305,7 +305,7 @@ module "ecs_service" { | [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\_configuration](#input\_service\_connect\_configuration) | The ECS Service Connect configuration for this service to discover and connect to services, and be discovered by, and connected from, other services within a namespace |
object({
enabled = optional(bool, true)
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(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)
}))
})))
})
| `null` | no | +| [service\_connect\_configuration](#input\_service\_connect\_configuration) | The ECS Service Connect configuration for this service to discover and connect to services, and be discovered by, and connected from, other services within a namespace |
object({
enabled = optional(bool, true)
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)
}))
})))
})
| `null` | no | | [service\_registries](#input\_service\_registries) | Service discovery registries for the service |
object({
container_name = optional(string)
container_port = optional(number)
port = optional(number)
registry_arn = string
})
| `null` | no | | [service\_tags](#input\_service\_tags) | A map of additional tags to add to the service | `map(string)` | `{}` | no | | [skip\_destroy](#input\_skip\_destroy) | If true, the task is not deleted when the service is deleted | `bool` | `null` | no | diff --git a/modules/service/main.tf b/modules/service/main.tf index 072bf28..53b7da8 100644 --- a/modules/service/main.tf +++ b/modules/service/main.tf @@ -81,7 +81,7 @@ resource "aws_ecs_service" "this" { bake_time_in_minutes = try(deployment_configuration.value.bake_time_in_minutes, null) dynamic "lifecycle_hook" { - for_each = deployment_configuration.value.lifecycle_hook != null ? [deployment_configuration.value.lifecycle_hook] : [] + for_each = deployment_configuration.value.lifecycle_hook != null ? deployment_configuration.value.lifecycle_hook : {} content { hook_target_arn = lifecycle_hook.value.hook_target_arn @@ -208,7 +208,7 @@ resource "aws_ecs_service" "this" { port = client_alias.value.port dynamic "test_traffic_rules" { - for_each = client_alias.value.test_traffic_rules != null ? [client_alias.value.test_traffic_rules] : [] + for_each = client_alias.value.test_traffic_rules != null ? client_alias.value.test_traffic_rules : [] content { dynamic "header" { @@ -401,7 +401,7 @@ resource "aws_ecs_service" "ignore_task_definition" { bake_time_in_minutes = try(deployment_configuration.value.bake_time_in_minutes, null) dynamic "lifecycle_hook" { - for_each = deployment_configuration.value.lifecycle_hook != null ? [deployment_configuration.value.lifecycle_hook] : [] + for_each = deployment_configuration.value.lifecycle_hook != null ? deployment_configuration.value.lifecycle_hook : {} content { hook_target_arn = lifecycle_hook.value.hook_target_arn @@ -528,7 +528,7 @@ resource "aws_ecs_service" "ignore_task_definition" { port = client_alias.value.port dynamic "test_traffic_rules" { - for_each = client_alias.value.test_traffic_rules != null ? [client_alias.value.test_traffic_rules] : [] + for_each = client_alias.value.test_traffic_rules != null ? client_alias.value.test_traffic_rules : [] content { dynamic "header" { diff --git a/modules/service/variables.tf b/modules/service/variables.tf index f5e8bca..0a49c7a 100644 --- a/modules/service/variables.tf +++ b/modules/service/variables.tf @@ -78,11 +78,11 @@ variable "deployment_configuration" { type = object({ strategy = optional(string) bake_time_in_minutes = optional(string) - lifecycle_hook = optional(object({ + lifecycle_hook = optional(map(object({ hook_target_arn = string role_arn = string - lifecycle_stages = string - })) + lifecycle_stages = list(string) + }))) }) default = null } @@ -243,14 +243,14 @@ variable "service_connect_configuration" { client_alias = optional(object({ dns_name = optional(string) port = number - test_traffic_rules = optional(object({ + test_traffic_rules = optional(list(object({ header = optional(object({ name = string value = object({ exact = string }) })) - })) + }))) })) discovery_name = optional(string) ingress_port_override = optional(number) diff --git a/variables.tf b/variables.tf index cb0a9ec..47b5928 100644 --- a/variables.tf +++ b/variables.tf @@ -283,11 +283,11 @@ variable "services" { deployment_configuration = optional(object({ strategy = optional(string) bake_time_in_minutes = optional(string) - lifecycle_hook = optional(object({ + lifecycle_hook = optional(map(object({ hook_target_arn = string role_arn = string - lifecycle_stages = string - })) + lifecycle_stages = list(string) + }))) })) deployment_controller = optional(object({ type = optional(string) @@ -343,14 +343,14 @@ variable "services" { client_alias = optional(object({ dns_name = optional(string) port = number - test_traffic_rules = optional(object({ + test_traffic_rules = optional(list(object({ header = optional(object({ name = string value = object({ exact = string }) })) - })) + }))) })) discovery_name = optional(string) ingress_port_override = optional(number) From 4c872f10eb1bebe9b7d17a2d383518db897730f4 Mon Sep 17 00:00:00 2001 From: magreenbaum Date: Sun, 20 Jul 2025 19:42:33 -0400 Subject: [PATCH 3/6] update example --- README.md | 1 + examples/blue-green/README.md | 80 ++++++ examples/blue-green/main.tf | 405 +++++++++++++++++++++++++++++++ examples/blue-green/outputs.tf | 46 ++++ examples/blue-green/variables.tf | 0 examples/blue-green/versions.tf | 10 + 6 files changed, 542 insertions(+) create mode 100644 examples/blue-green/README.md create mode 100644 examples/blue-green/main.tf create mode 100644 examples/blue-green/outputs.tf create mode 100644 examples/blue-green/variables.tf create mode 100644 examples/blue-green/versions.tf diff --git a/README.md b/README.md index 394d4c1..b9ad52e 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ module "ecs" { - [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/ Blue/Green Deployment](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/blue-green) ## Requirements diff --git a/examples/blue-green/README.md b/examples/blue-green/README.md new file mode 100644 index 0000000..66292ea --- /dev/null +++ b/examples/blue-green/README.md @@ -0,0 +1,80 @@ +# ECS Cluster Blue/Green Deployment + +Configuration in this directory creates: + +- ECS cluster using Fargate (on-demand and spot) capacity providers +- Example ECS service that utilizes + - Service Blue/Green Deployment and related listener/listener rules, target groups, IAM permissions + - 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.4 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 6.4 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [alb](#module\_alb) | terraform-aws-modules/alb/aws | ~> 9.0 | +| [autoscaling](#module\_autoscaling) | terraform-aws-modules/autoscaling/aws | ~> 9.0 | +| [autoscaling\_sg](#module\_autoscaling\_sg) | terraform-aws-modules/security-group/aws | ~> 5.0 | +| [ecs](#module\_ecs) | ../../ | n/a | +| [ecs\_cluster\_disabled](#module\_ecs\_cluster\_disabled) | ../../modules/cluster | n/a | +| [ecs\_disabled](#module\_ecs\_disabled) | ../../ | n/a | +| [service\_disabled](#module\_service\_disabled) | ../../modules/service | n/a | +| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 6.0 | + +## Resources + +| Name | Type | +|------|------| +| [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.ecs_optimized_ami](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.fluentbit](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +No inputs. + +## Outputs + +| Name | Description | +|------|-------------| +| [alb\_dns\_name](#output\_alb\_dns\_name) | The DNS name of the load balancer | +| [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 | +| [services](#output\_services) | Map of services created and their attributes | + + +## License + +Apache-2.0 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-ecs/blob/master/LICENSE). diff --git a/examples/blue-green/main.tf b/examples/blue-green/main.tf new file mode 100644 index 0000000..9898ab8 --- /dev/null +++ b/examples/blue-green/main.tf @@ -0,0 +1,405 @@ +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 = "ecsdemo-frontend" + container_port = 3000 + + tags = { + Name = local.name + Example = local.name + Repository = "https://github.com/terraform-aws-modules/terraform-aws-ecs" + } +} + +################################################################################ +# Cluster +################################################################################ + +module "ecs" { + source = "../../" + + cluster_name = local.name + + # Cluster capacity providers + default_capacity_provider_strategy = { + FARGATE = { + weight = 50 + base = 20 + } + FARGATE_SPOT = { + weight = 50 + } + } + + services = { + ecsdemo-frontend = { + cpu = 1024 + memory = 4096 + + # for blue/green deployments + deployment_configuration = { + strategy = "BLUE_GREEN" + bake_time_in_minutes = 2 + + # example config using lifecycle hooks + # lifecycle_hook = { + # success = { + # hook_target_arn = aws_lambda_function.hook_success.arn + # role_arn = aws_iam_role.global.arn + # lifecycle_stages = ["POST_SCALE_UP", "POST_TEST_TRAFFIC_SHIFT"] + # } + # failure = { + # hook_target_arn = aws_lambda_function.hook_success.arn + # role_arn = aws_iam_role.global.arn + # lifecycle_stages = ["TEST_TRAFFIC_SHIFT", "POST_PRODUCTION_TRAFFIC_SHIFT"] + # } + # } + } + + # Container definition(s) + container_definitions = { + + fluent-bit = { + cpu = 512 + memory = 1024 + essential = true + image = nonsensitive(data.aws_ssm_parameter.fluentbit.value) + user = "0" + firelensConfiguration = { + type = "fluentbit" + } + memoryReservation = 50 + + cloudwatch_log_group_retention_in_days = 30 + } + + (local.container_name) = { + cpu = 512 + memory = 1024 + essential = true + image = "public.ecr.aws/aws-containers/ecsdemo-frontend:776fd50" + + healthCheck = { + command = ["CMD-SHELL", "curl -f http://localhost:${local.container_port}/health || exit 1"] + } + + portMappings = [ + { + name = local.container_name + containerPort = local.container_port + hostPort = local.container_port + protocol = "tcp" + } + ] + + capacity_provider_strategy = { + ASG = { + base = 20 + capacity_provider = "ASG" + weight = 50 + } + } + + # Example image used requires access to write to root filesystem + readonlyRootFilesystem = false + + dependsOn = [{ + containerName = "fluent-bit" + condition = "START" + }] + + enable_cloudwatch_logging = false + logConfiguration = { + logDriver = "awsfirelens" + options = { + Name = "firehose" + region = local.region + delivery_stream = "my-stream" + log-driver-buffer-limit = "2097152" + } + } + memoryReservation = 100 + + restartPolicy = { + enabled = true + ignoredExitCodes = [1] + restartAttemptPeriod = 60 + } + } + } + + service_connect_configuration = { + namespace = aws_service_discovery_http_namespace.this.arn + service = [ + { + client_alias = { + port = local.container_port + dns_name = local.container_name + } + + port_name = local.container_name + discovery_name = local.container_name + } + ] + } + + load_balancer = { + service = { + 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 + role_arn = aws_iam_role.ecs_elb_permissions.arn + } + } + } + + subnet_ids = module.vpc.private_subnets + availability_zone_rebalancing = "ENABLED" + security_group_ingress_rules = { + alb_3000 = { + 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" + } + } + } + } + + tags = local.tags +} + +################################################################################ +# Supporting Resources +################################################################################ + +data "aws_ssm_parameter" "fluentbit" { + name = "/aws/service/aws-for-fluent-bit/stable" +} + +resource "aws_service_discovery_http_namespace" "this" { + name = local.name + description = "CloudMap namespace for ${local.name}" + tags = local.tags +} + +module "alb" { + source = "terraform-aws-modules/alb/aws" + version = "~> 9.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" + + fixed_response = { + content_type = "text/plain" + message_body = "404: Page not found" + status_code = "404" + } + + # for blue/green deployments + rules = { + production = { + priority = 1 + actions = [ + { + type = "weighted-forward" + target_groups = [ + { + target_group_key = "ex_ecs" + weight = 100 + }, + { + target_group_key = "ex_ecs_alternate" + weight = 0 + } + ] + } + ] + conditions = [ + { + path_pattern = { + values = ["/*"] + } + } + ] + } + test = { + priority = 2 + actions = [ + { + type = "weighted-forward" + target_groups = [ + { + target_group_key = "ex_ecs_alternate" + weight = 100 + } + ] + } + ] + conditions = [ + { + path_pattern = { + values = ["/*"] + } + } + ] + } + } + } + } + + 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 + } + + # for blue/green deployments + ex_ecs_alternate = { + 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 + } + + # There's 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 + + depends_on = [ + aws_iam_role.ecs_elb_permissions, + aws_iam_role_policy_attachment.ecs_elb_management_role, + aws_iam_role_policy_attachment.ecs_service_role + ] +} + +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 +} + +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/examples/blue-green/outputs.tf b/examples/blue-green/outputs.tf new file mode 100644 index 0000000..dd0cdd2 --- /dev/null +++ b/examples/blue-green/outputs.tf @@ -0,0 +1,46 @@ +################################################################################ +# 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 "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 +} + +################################################################################ +# Service(s) +################################################################################ + +output "services" { + description = "Map of services created and their attributes" + value = module.ecs.services +} + +################################################################################ +# Application Load Balancer +################################################################################ + +output "alb_dns_name" { + description = "The DNS name of the load balancer" + value = module.alb.dns_name +} diff --git a/examples/blue-green/variables.tf b/examples/blue-green/variables.tf new file mode 100644 index 0000000..e69de29 diff --git a/examples/blue-green/versions.tf b/examples/blue-green/versions.tf new file mode 100644 index 0000000..497e3e6 --- /dev/null +++ b/examples/blue-green/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.5.7" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.4" + } + } +} From a466213fa0123e9123ee46542f8a940988e740e4 Mon Sep 17 00:00:00 2001 From: magreenbaum Date: Sun, 20 Jul 2025 19:48:15 -0400 Subject: [PATCH 4/6] update example --- examples/blue-green/README.md | 9 +++------ examples/blue-green/main.tf | 16 ++++++++-------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/examples/blue-green/README.md b/examples/blue-green/README.md index 66292ea..5422f92 100644 --- a/examples/blue-green/README.md +++ b/examples/blue-green/README.md @@ -41,21 +41,18 @@ Note that this example may create resources which will incur monetary charges on | Name | Source | Version | |------|--------|---------| | [alb](#module\_alb) | terraform-aws-modules/alb/aws | ~> 9.0 | -| [autoscaling](#module\_autoscaling) | terraform-aws-modules/autoscaling/aws | ~> 9.0 | -| [autoscaling\_sg](#module\_autoscaling\_sg) | terraform-aws-modules/security-group/aws | ~> 5.0 | | [ecs](#module\_ecs) | ../../ | n/a | -| [ecs\_cluster\_disabled](#module\_ecs\_cluster\_disabled) | ../../modules/cluster | n/a | -| [ecs\_disabled](#module\_ecs\_disabled) | ../../ | n/a | -| [service\_disabled](#module\_service\_disabled) | ../../modules/service | n/a | | [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 6.0 | ## Resources | 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.ecs_optimized_ami](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.fluentbit](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | ## Inputs diff --git a/examples/blue-green/main.tf b/examples/blue-green/main.tf index 9898ab8..7d9cf58 100644 --- a/examples/blue-green/main.tf +++ b/examples/blue-green/main.tf @@ -59,7 +59,7 @@ module "ecs" { # lifecycle_stages = ["POST_SCALE_UP", "POST_TEST_TRAFFIC_SHIFT"] # } # failure = { - # hook_target_arn = aws_lambda_function.hook_success.arn + # hook_target_arn = aws_lambda_function.hook_failure.arn # role_arn = aws_iam_role.global.arn # lifecycle_stages = ["TEST_TRAFFIC_SHIFT", "POST_PRODUCTION_TRAFFIC_SHIFT"] # } @@ -249,17 +249,17 @@ module "alb" { rules = { production = { priority = 1 - actions = [ + actions = [ { - type = "weighted-forward" + type = "weighted-forward" target_groups = [ { target_group_key = "ex_ecs" - weight = 100 + weight = 100 }, { target_group_key = "ex_ecs_alternate" - weight = 0 + weight = 0 } ] } @@ -274,13 +274,13 @@ module "alb" { } test = { priority = 2 - actions = [ + actions = [ { - type = "weighted-forward" + type = "weighted-forward" target_groups = [ { target_group_key = "ex_ecs_alternate" - weight = 100 + weight = 100 } ] } From 1a4588c938b11bd1d5a39bb4fb218499182c9a80 Mon Sep 17 00:00:00 2001 From: magreenbaum Date: Sun, 20 Jul 2025 21:21:39 -0400 Subject: [PATCH 5/6] update examples --- README.md | 1 - examples/blue-green/README.md | 77 ------ examples/blue-green/main.tf | 405 ------------------------------- examples/blue-green/outputs.tf | 46 ---- examples/blue-green/variables.tf | 0 examples/blue-green/versions.tf | 10 - examples/fargate/README.md | 3 + examples/fargate/main.tf | 145 ++++++++++- 8 files changed, 146 insertions(+), 541 deletions(-) delete mode 100644 examples/blue-green/README.md delete mode 100644 examples/blue-green/main.tf delete mode 100644 examples/blue-green/outputs.tf delete mode 100644 examples/blue-green/variables.tf delete mode 100644 examples/blue-green/versions.tf diff --git a/README.md b/README.md index b9ad52e..394d4c1 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,6 @@ module "ecs" { - [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/ Blue/Green Deployment](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/blue-green) ## Requirements diff --git a/examples/blue-green/README.md b/examples/blue-green/README.md deleted file mode 100644 index 5422f92..0000000 --- a/examples/blue-green/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# ECS Cluster Blue/Green Deployment - -Configuration in this directory creates: - -- ECS cluster using Fargate (on-demand and spot) capacity providers -- Example ECS service that utilizes - - Service Blue/Green Deployment and related listener/listener rules, target groups, IAM permissions - - 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.4 | - -## Providers - -| Name | Version | -|------|---------| -| [aws](#provider\_aws) | >= 6.4 | - -## Modules - -| Name | Source | Version | -|------|--------|---------| -| [alb](#module\_alb) | terraform-aws-modules/alb/aws | ~> 9.0 | -| [ecs](#module\_ecs) | ../../ | n/a | -| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 6.0 | - -## Resources - -| 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 | - -## Inputs - -No inputs. - -## Outputs - -| Name | Description | -|------|-------------| -| [alb\_dns\_name](#output\_alb\_dns\_name) | The DNS name of the load balancer | -| [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 | -| [services](#output\_services) | Map of services created and their attributes | - - -## License - -Apache-2.0 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-ecs/blob/master/LICENSE). diff --git a/examples/blue-green/main.tf b/examples/blue-green/main.tf deleted file mode 100644 index 7d9cf58..0000000 --- a/examples/blue-green/main.tf +++ /dev/null @@ -1,405 +0,0 @@ -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 = "ecsdemo-frontend" - container_port = 3000 - - tags = { - Name = local.name - Example = local.name - Repository = "https://github.com/terraform-aws-modules/terraform-aws-ecs" - } -} - -################################################################################ -# Cluster -################################################################################ - -module "ecs" { - source = "../../" - - cluster_name = local.name - - # Cluster capacity providers - default_capacity_provider_strategy = { - FARGATE = { - weight = 50 - base = 20 - } - FARGATE_SPOT = { - weight = 50 - } - } - - services = { - ecsdemo-frontend = { - cpu = 1024 - memory = 4096 - - # for blue/green deployments - deployment_configuration = { - strategy = "BLUE_GREEN" - bake_time_in_minutes = 2 - - # example config using lifecycle hooks - # lifecycle_hook = { - # success = { - # hook_target_arn = aws_lambda_function.hook_success.arn - # role_arn = aws_iam_role.global.arn - # lifecycle_stages = ["POST_SCALE_UP", "POST_TEST_TRAFFIC_SHIFT"] - # } - # failure = { - # hook_target_arn = aws_lambda_function.hook_failure.arn - # role_arn = aws_iam_role.global.arn - # lifecycle_stages = ["TEST_TRAFFIC_SHIFT", "POST_PRODUCTION_TRAFFIC_SHIFT"] - # } - # } - } - - # Container definition(s) - container_definitions = { - - fluent-bit = { - cpu = 512 - memory = 1024 - essential = true - image = nonsensitive(data.aws_ssm_parameter.fluentbit.value) - user = "0" - firelensConfiguration = { - type = "fluentbit" - } - memoryReservation = 50 - - cloudwatch_log_group_retention_in_days = 30 - } - - (local.container_name) = { - cpu = 512 - memory = 1024 - essential = true - image = "public.ecr.aws/aws-containers/ecsdemo-frontend:776fd50" - - healthCheck = { - command = ["CMD-SHELL", "curl -f http://localhost:${local.container_port}/health || exit 1"] - } - - portMappings = [ - { - name = local.container_name - containerPort = local.container_port - hostPort = local.container_port - protocol = "tcp" - } - ] - - capacity_provider_strategy = { - ASG = { - base = 20 - capacity_provider = "ASG" - weight = 50 - } - } - - # Example image used requires access to write to root filesystem - readonlyRootFilesystem = false - - dependsOn = [{ - containerName = "fluent-bit" - condition = "START" - }] - - enable_cloudwatch_logging = false - logConfiguration = { - logDriver = "awsfirelens" - options = { - Name = "firehose" - region = local.region - delivery_stream = "my-stream" - log-driver-buffer-limit = "2097152" - } - } - memoryReservation = 100 - - restartPolicy = { - enabled = true - ignoredExitCodes = [1] - restartAttemptPeriod = 60 - } - } - } - - service_connect_configuration = { - namespace = aws_service_discovery_http_namespace.this.arn - service = [ - { - client_alias = { - port = local.container_port - dns_name = local.container_name - } - - port_name = local.container_name - discovery_name = local.container_name - } - ] - } - - load_balancer = { - service = { - 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 - role_arn = aws_iam_role.ecs_elb_permissions.arn - } - } - } - - subnet_ids = module.vpc.private_subnets - availability_zone_rebalancing = "ENABLED" - security_group_ingress_rules = { - alb_3000 = { - 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" - } - } - } - } - - tags = local.tags -} - -################################################################################ -# Supporting Resources -################################################################################ - -data "aws_ssm_parameter" "fluentbit" { - name = "/aws/service/aws-for-fluent-bit/stable" -} - -resource "aws_service_discovery_http_namespace" "this" { - name = local.name - description = "CloudMap namespace for ${local.name}" - tags = local.tags -} - -module "alb" { - source = "terraform-aws-modules/alb/aws" - version = "~> 9.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" - - fixed_response = { - content_type = "text/plain" - message_body = "404: Page not found" - status_code = "404" - } - - # for blue/green deployments - rules = { - production = { - priority = 1 - actions = [ - { - type = "weighted-forward" - target_groups = [ - { - target_group_key = "ex_ecs" - weight = 100 - }, - { - target_group_key = "ex_ecs_alternate" - weight = 0 - } - ] - } - ] - conditions = [ - { - path_pattern = { - values = ["/*"] - } - } - ] - } - test = { - priority = 2 - actions = [ - { - type = "weighted-forward" - target_groups = [ - { - target_group_key = "ex_ecs_alternate" - weight = 100 - } - ] - } - ] - conditions = [ - { - path_pattern = { - values = ["/*"] - } - } - ] - } - } - } - } - - 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 - } - - # for blue/green deployments - ex_ecs_alternate = { - 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 - } - - # There's 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 - - depends_on = [ - aws_iam_role.ecs_elb_permissions, - aws_iam_role_policy_attachment.ecs_elb_management_role, - aws_iam_role_policy_attachment.ecs_service_role - ] -} - -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 -} - -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/examples/blue-green/outputs.tf b/examples/blue-green/outputs.tf deleted file mode 100644 index dd0cdd2..0000000 --- a/examples/blue-green/outputs.tf +++ /dev/null @@ -1,46 +0,0 @@ -################################################################################ -# 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 "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 -} - -################################################################################ -# Service(s) -################################################################################ - -output "services" { - description = "Map of services created and their attributes" - value = module.ecs.services -} - -################################################################################ -# Application Load Balancer -################################################################################ - -output "alb_dns_name" { - description = "The DNS name of the load balancer" - value = module.alb.dns_name -} diff --git a/examples/blue-green/variables.tf b/examples/blue-green/variables.tf deleted file mode 100644 index e69de29..0000000 diff --git a/examples/blue-green/versions.tf b/examples/blue-green/versions.tf deleted file mode 100644 index 497e3e6..0000000 --- a/examples/blue-green/versions.tf +++ /dev/null @@ -1,10 +0,0 @@ -terraform { - required_version = ">= 1.5.7" - - required_providers { - aws = { - source = "hashicorp/aws" - version = ">= 6.4" - } - } -} diff --git a/examples/fargate/README.md b/examples/fargate/README.md index 0b6ef0f..2a74d68 100644 --- a/examples/fargate/README.md +++ b/examples/fargate/README.md @@ -49,6 +49,9 @@ 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 b84c738..f52e2c6 100644 --- a/examples/fargate/main.tf +++ b/examples/fargate/main.tf @@ -60,6 +60,26 @@ module "ecs_service" { # Enables ECS Exec enable_execute_command = true + # for blue/green deployments + deployment_configuration = { + strategy = "BLUE_GREEN" + bake_time_in_minutes = 2 + + # example config using lifecycle hooks + # lifecycle_hook = { + # success = { + # hook_target_arn = aws_lambda_function.hook_success.arn + # role_arn = aws_iam_role.global.arn + # lifecycle_stages = ["POST_SCALE_UP", "POST_TEST_TRAFFIC_SHIFT"] + # } + # failure = { + # hook_target_arn = aws_lambda_function.hook_failure.arn + # role_arn = aws_iam_role.global.arn + # lifecycle_stages = ["TEST_TRAFFIC_SHIFT", "POST_PRODUCTION_TRAFFIC_SHIFT"] + # } + # } + } + # Container definition(s) container_definitions = { @@ -152,6 +172,14 @@ module "ecs_service" { 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 + role_arn = aws_iam_role.ecs_elb_permissions.arn + } } } @@ -176,6 +204,12 @@ 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 + ] } ################################################################################ @@ -278,8 +312,60 @@ module "alb" { port = 80 protocol = "HTTP" - forward = { - target_group_key = "ex_ecs" + fixed_response = { + content_type = "text/plain" + message_body = "404: Page not found" + status_code = "404" + } + + # for blue/green deployments + rules = { + production = { + priority = 1 + actions = [ + { + type = "weighted-forward" + target_groups = [ + { + target_group_key = "ex_ecs" + weight = 100 + }, + { + target_group_key = "ex_ecs_alternate" + weight = 0 + } + ] + } + ] + conditions = [ + { + path_pattern = { + values = ["/*"] + } + } + ] + } + test = { + priority = 2 + actions = [ + { + type = "weighted-forward" + target_groups = [ + { + target_group_key = "ex_ecs_alternate" + weight = 100 + } + ] + } + ] + conditions = [ + { + path_pattern = { + values = ["/*"] + } + } + ] + } } } } @@ -308,6 +394,31 @@ module "alb" { # ECS will attach the IPs of the tasks to this target group create_attachment = false } + + # for blue/green deployments + ex_ecs_alternate = { + 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 + } + + # There's 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 @@ -329,3 +440,33 @@ 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" +} From d48b3a0c97a76ba16cd46d580a1f39171bf32f87 Mon Sep 17 00:00:00 2001 From: magreenbaum Date: Mon, 21 Jul 2025 16:30:39 -0400 Subject: [PATCH 6/6] feedback changes --- modules/service/main.tf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/service/main.tf b/modules/service/main.tf index 53b7da8..6119c86 100644 --- a/modules/service/main.tf +++ b/modules/service/main.tf @@ -77,8 +77,8 @@ resource "aws_ecs_service" "this" { for_each = var.deployment_configuration != null ? [var.deployment_configuration] : [] content { - strategy = try(deployment_configuration.value.strategy, null) - bake_time_in_minutes = try(deployment_configuration.value.bake_time_in_minutes, null) + strategy = deployment_configuration.value.strategy + bake_time_in_minutes = deployment_configuration.value.bake_time_in_minutes dynamic "lifecycle_hook" { for_each = deployment_configuration.value.lifecycle_hook != null ? deployment_configuration.value.lifecycle_hook : {} @@ -128,7 +128,7 @@ resource "aws_ecs_service" "this" { alternate_target_group_arn = advanced_configuration.value.alternate_target_group_arn production_listener_rule = advanced_configuration.value.production_listener_rule role_arn = advanced_configuration.value.role_arn - test_listener_rule = try(advanced_configuration.value.test_listener_rule, null) + test_listener_rule = advanced_configuration.value.test_listener_rule } } } @@ -397,8 +397,8 @@ resource "aws_ecs_service" "ignore_task_definition" { for_each = var.deployment_configuration != null ? [var.deployment_configuration] : [] content { - strategy = try(deployment_configuration.value.strategy, null) - bake_time_in_minutes = try(deployment_configuration.value.bake_time_in_minutes, null) + strategy = deployment_configuration.value.strategy + bake_time_in_minutes = deployment_configuration.value.bake_time_in_minutes dynamic "lifecycle_hook" { for_each = deployment_configuration.value.lifecycle_hook != null ? deployment_configuration.value.lifecycle_hook : {} @@ -448,7 +448,7 @@ resource "aws_ecs_service" "ignore_task_definition" { alternate_target_group_arn = advanced_configuration.value.alternate_target_group_arn production_listener_rule = advanced_configuration.value.production_listener_rule role_arn = advanced_configuration.value.role_arn - test_listener_rule = try(advanced_configuration.value.test_listener_rule, null) + test_listener_rule = advanced_configuration.value.test_listener_rule } } }