Skip to content
Merged
59 changes: 58 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ The service sub-module creates one service that can be deployed onto a cluster.

- Create an Amazon ECS service that ignores `desired_count`. This is intended for use when deploying task definition and container definition changes via Terraform
- Create an Amazon ECS service that ignores `desired_count` and `task_definition`. This is intended to support a continuous deployment process that is responsible for updating the `image` and therefore the `task_definition` and `container_definition` while avoiding conflicts with Terraform.
- Create an Amazon ECS service that ignores `desired_count`, `task_definition` and `load_balancer`. This is intended to support a continuous deployment process using [Blue/Green deployment with CodeDeploy](https://docs.aws.amazon.com/AmazonECS/latest/userguide/deployment-type-bluegreen.html) which will allow updating the `target_group_arn` while avoiding conflicts with Terraform.
- Amazon ECS task resources with the various configurations detailed below under [ECS Task](https://github.com/terraform-aws-modules/terraform-aws-ecs/blob/master/docs/README.md#ecs-task)

Since Terraform does not support variables within `lifecycle {}` blocks, its not possible to allow users to dynamically select which arguments they wish to ignore within the resources defined in the modules. Therefore, any arguments that should be ignored are statically set within the module definition. To somewhat mimic the behavior of allowing users to opt in/out of ignoring certain arguments, the module supports two different service definitions; one that ignores the `desired_count`, and one that ignores the `desired_count` and `task_definition`. The motivation and reasoning for these ignored argument configurations is detailed below:
Since Terraform does not support variables within `lifecycle {}` blocks, its not possible to allow users to dynamically select which arguments they wish to ignore within the resources defined in the modules. Therefore, any arguments that should be ignored are statically set within the module definition. To somewhat mimic the behavior of allowing users to opt in/out of ignoring certain arguments, the module supports three different service definitions; one that ignores the `desired_count`, and one that ignores the `desired_count` and `task_definition` and one that ignores `desired_count`, `task_definition` and `load_balancer`. The motivation and reasoning for these ignored argument configurations is detailed below:

- `desired_count` is always ignored by the service module. It is very common to have autoscaling enabled for Amazon ECS services, allowing the number of tasks to scale based on the workload requirements. The scaling is managed via the `desired_count` that is managed by application auto scaling. This would directly conflict with Terraform if it was allowed to manage the `desired_count` as well. In addition, users have the ability to disable auto scaling if it does not suit their workload. In this case, the `desired_count` would be initially set by Terraform, and any further changes would need to be managed separately (outside of the service module). Users can make changes to the desired count of the service through the AWS console, AWS CLI, or AWS SDKs. One example workaround using Terraform is provided below, similar to the [EKS equivalent](https://github.com/bryantbiggs/eks-desired-size-hack):

Expand Down Expand Up @@ -143,6 +144,62 @@ This could be expanded further to include the entire container definitions argum
<img src="./images/service.png" alt="ECS Service" width="40%">
</p>

- When using the above `ignore_task_definition_changes` setting, users can also elect to ignore changes to load balancers by setting `ignore_load_balancer_changes` to `true`. (Note: because of the aforementioned manner in which this psuedo-dynamic ignore change is being managed, changing this value after service creation will cause the entire service to be re-created. Change with caution!) This is intended to support the use of [Blue/Green deployment with CodeDeploy](https://docs.aws.amazon.com/AmazonECS/latest/userguide/deployment-type-bluegreen.html) which changes the the service's load balancer configuration.

```hcl

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

module "ecs_service" {
source = "terraform-aws-modules/ecs/aws//modules/service"

# ... omitted for brevity

ignore_task_definition_changes = true
ignore_load_balancer_changes = true
}

resource "aws_lb_target_group" "this" {
for_each = { blue = {}, green = {} }
name = each.key

# ... omitted for brevity
}

resource "aws_codedeploy_app" "this" {
name = "my-app"
compute_platform = "ECS"
}

resource "aws_codedeploy_deployment_group" "this" {
deployment_group_name = "my-deployment-group"
app_name = aws_codedeploy_app.this.name

deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"

deployment_style {
deployment_option = "WITH_TRAFFIC_CONTROL"
deployment_type = "BLUE_GREEN"
}

# ... omitted for brevity

load_balancer_info {
target_group_pair_info {
prod_traffic_route {
listener_arns = ["my-listener-arn"]
}

target_group {
name = aws_lb_target_group.this["blue"].name
}

target_group {
name = aws_lb_target_group.this["green"].name
}
}
}
}
```

### Task

ECS tasks are the byproduct of task definitions and task sets. In addition to what has been described above, the service module supports the following task level configurations:
Expand Down
2 changes: 2 additions & 0 deletions modules/service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ module "ecs_service" {
| [aws_appautoscaling_scheduled_action.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/appautoscaling_scheduled_action) | resource |
| [aws_appautoscaling_target.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/appautoscaling_target) | resource |
| [aws_ecs_service.ignore_task_definition](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service) | resource |
| [aws_ecs_service.ignore_task_definition_load_balancer](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service) | resource |
| [aws_ecs_service.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service) | resource |
| [aws_ecs_task_definition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) | resource |
| [aws_ecs_task_set.ignore_task_definition](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_set) | resource |
Expand Down Expand Up @@ -258,6 +259,7 @@ module "ecs_service" {
| <a name="input_iam_role_statements"></a> [iam\_role\_statements](#input\_iam\_role\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage | `any` | `{}` | no |
| <a name="input_iam_role_tags"></a> [iam\_role\_tags](#input\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no |
| <a name="input_iam_role_use_name_prefix"></a> [iam\_role\_use\_name\_prefix](#input\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`iam_role_name`) is used as a prefix | `bool` | `true` | no |
| <a name="input_ignore_load_balancer_changes"></a> [ignore\_load\_balancer\_changes](#input\_ignore\_load\_balancer\_changes) | Whether changes to service `load_balancer` changes should be ignored | `bool` | `false` | no |
| <a name="input_ignore_task_definition_changes"></a> [ignore\_task\_definition\_changes](#input\_ignore\_task\_definition\_changes) | Whether changes to service `task_definition` changes should be ignored | `bool` | `false` | no |
| <a name="input_inference_accelerator"></a> [inference\_accelerator](#input\_inference\_accelerator) | Configuration block(s) with Inference Accelerators settings | `any` | `{}` | no |
| <a name="input_ipc_mode"></a> [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 |
Expand Down
192 changes: 190 additions & 2 deletions modules/service/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ locals {
}

resource "aws_ecs_service" "this" {
count = var.create && !var.ignore_task_definition_changes ? 1 : 0
count = var.create && !var.ignore_task_definition_changes && !var.ignore_load_balancer_changes ? 1 : 0

dynamic "alarms" {
for_each = length(var.alarms) > 0 ? [var.alarms] : []
Expand Down Expand Up @@ -214,7 +214,7 @@ resource "aws_ecs_service" "this" {
################################################################################

resource "aws_ecs_service" "ignore_task_definition" {
count = var.create && var.ignore_task_definition_changes ? 1 : 0
count = var.create && var.ignore_task_definition_changes && !var.ignore_load_balancer_changes ? 1 : 0

dynamic "alarms" {
for_each = length(var.alarms) > 0 ? [var.alarms] : []
Expand Down Expand Up @@ -396,6 +396,194 @@ resource "aws_ecs_service" "ignore_task_definition" {
}
}

################################################################################
# Service - Ignore `task_definition` and `load_balancer`
################################################################################

resource "aws_ecs_service" "ignore_task_definition_load_balancer" {
count = var.create && var.ignore_task_definition_changes && var.ignore_load_balancer_changes ? 1 : 0

dynamic "alarms" {
for_each = length(var.alarms) > 0 ? [var.alarms] : []

content {
alarm_names = alarms.value.alarm_names
enable = try(alarms.value.enable, true)
rollback = try(alarms.value.rollback, true)
}
}

dynamic "capacity_provider_strategy" {
# Set by task set if deployment controller is external
for_each = { for k, v in var.capacity_provider_strategy : k => v if !local.is_external_deployment }

content {
base = try(capacity_provider_strategy.value.base, null)
capacity_provider = capacity_provider_strategy.value.capacity_provider
weight = try(capacity_provider_strategy.value.weight, null)
}
}

cluster = var.cluster_arn

dynamic "deployment_circuit_breaker" {
for_each = length(var.deployment_circuit_breaker) > 0 ? [var.deployment_circuit_breaker] : []

content {
enable = deployment_circuit_breaker.value.enable
rollback = deployment_circuit_breaker.value.rollback
}
}

dynamic "deployment_controller" {
for_each = length(var.deployment_controller) > 0 ? [var.deployment_controller] : []

content {
type = try(deployment_controller.value.type, null)
}
}

deployment_maximum_percent = local.is_daemon || local.is_external_deployment ? null : var.deployment_maximum_percent
deployment_minimum_healthy_percent = local.is_daemon || local.is_external_deployment ? null : var.deployment_minimum_healthy_percent
desired_count = local.is_daemon || local.is_external_deployment ? null : var.desired_count
enable_ecs_managed_tags = var.enable_ecs_managed_tags
enable_execute_command = var.enable_execute_command
force_new_deployment = local.is_external_deployment ? null : var.force_new_deployment
health_check_grace_period_seconds = var.health_check_grace_period_seconds
iam_role = local.iam_role_arn
launch_type = local.is_external_deployment || length(var.capacity_provider_strategy) > 0 ? null : var.launch_type

dynamic "load_balancer" {
# Set by task set if deployment controller is external
for_each = { for k, v in var.load_balancer : k => v if !local.is_external_deployment }

content {
container_name = load_balancer.value.container_name
container_port = load_balancer.value.container_port
elb_name = try(load_balancer.value.elb_name, null)
target_group_arn = try(load_balancer.value.target_group_arn, null)
}
}

name = var.name

dynamic "network_configuration" {
# Set by task set if deployment controller is external
for_each = var.network_mode == "awsvpc" ? [{ for k, v in local.network_configuration : k => v if !local.is_external_deployment }] : []

content {
assign_public_ip = network_configuration.value.assign_public_ip
security_groups = network_configuration.value.security_groups
subnets = network_configuration.value.subnets
}
}

dynamic "ordered_placement_strategy" {
for_each = var.ordered_placement_strategy

content {
field = try(ordered_placement_strategy.value.field, null)
type = ordered_placement_strategy.value.type
}
}

dynamic "placement_constraints" {
for_each = var.placement_constraints

content {
expression = try(placement_constraints.value.expression, null)
type = placement_constraints.value.type
}
}

# Set by task set if deployment controller is external
platform_version = local.is_fargate && !local.is_external_deployment ? var.platform_version : null
scheduling_strategy = local.is_fargate ? "REPLICA" : var.scheduling_strategy

dynamic "service_connect_configuration" {
for_each = length(var.service_connect_configuration) > 0 ? [var.service_connect_configuration] : []

content {
enabled = try(service_connect_configuration.value.enabled, true)

dynamic "log_configuration" {
for_each = try([service_connect_configuration.value.log_configuration], [])

content {
log_driver = try(log_configuration.value.log_driver, null)
options = try(log_configuration.value.options, null)

dynamic "secret_option" {
for_each = try(log_configuration.value.secret_option, [])

content {
name = secret_option.value.name
value_from = secret_option.value.value_from
}
}
}
}

namespace = lookup(service_connect_configuration.value, "namespace", null)

dynamic "service" {
for_each = try([service_connect_configuration.value.service], [])

content {

dynamic "client_alias" {
for_each = try([service.value.client_alias], [])

content {
dns_name = try(client_alias.value.dns_name, null)
port = client_alias.value.port
}
}

discovery_name = try(service.value.discovery_name, null)
ingress_port_override = try(service.value.ingress_port_override, null)
port_name = service.value.port_name
}
}
}
}

dynamic "service_registries" {
# Set by task set if deployment controller is external
for_each = length(var.service_registries) > 0 ? [{ for k, v in var.service_registries : k => v if !local.is_external_deployment }] : []

content {
container_name = try(service_registries.value.container_name, null)
container_port = try(service_registries.value.container_port, null)
port = try(service_registries.value.port, null)
registry_arn = service_registries.value.registry_arn
}
}

task_definition = local.task_definition
triggers = var.triggers
wait_for_steady_state = var.wait_for_steady_state

propagate_tags = var.propagate_tags
tags = var.tags

timeouts {
create = try(var.timeouts.create, null)
update = try(var.timeouts.update, null)
delete = try(var.timeouts.delete, null)
}

depends_on = [aws_iam_role_policy_attachment.service]

lifecycle {
ignore_changes = [
desired_count, # Always ignored
task_definition,
load_balancer
]
}
}

################################################################################
# Service - IAM Role
################################################################################
Expand Down
2 changes: 1 addition & 1 deletion modules/service/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ output "id" {

output "name" {
description = "Name of the service"
value = try(aws_ecs_service.this[0].name, aws_ecs_service.ignore_task_definition[0].name, null)
value = try(aws_ecs_service.this[0].name, aws_ecs_service.ignore_task_definition[0].name, aws_ecs_service.ignore_task_definition_load_balancer[0].name, null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A similar change should be implemented for output id.

}

################################################################################
Expand Down
6 changes: 6 additions & 0 deletions modules/service/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ variable "ignore_task_definition_changes" {
default = false
}

variable "ignore_load_balancer_changes" {
description = "Whether changes to service `load_balancer` changes should be ignored"
type = bool
default = false
}

variable "alarms" {
description = "Information about the CloudWatch alarms"
type = any
Expand Down
1 change: 1 addition & 0 deletions wrappers/service/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module "wrapper" {
create = try(each.value.create, var.defaults.create, true)
tags = try(each.value.tags, var.defaults.tags, {})
ignore_task_definition_changes = try(each.value.ignore_task_definition_changes, var.defaults.ignore_task_definition_changes, false)
ignore_load_balancer_changes = try(each.value.ignore_load_balancer_changes, var.defaults.ignore_load_balancer_changes, false)
alarms = try(each.value.alarms, var.defaults.alarms, {})
capacity_provider_strategy = try(each.value.capacity_provider_strategy, var.defaults.capacity_provider_strategy, {})
cluster_arn = try(each.value.cluster_arn, var.defaults.cluster_arn, "")
Expand Down