Skip to content

Commit

Permalink
Fix listener certificate handling
Browse files Browse the repository at this point in the history
In the original design of this module, we assumed that each ECS
task had its own listener certificate and that these resources
could be applied and destroy along with the ECS task. This works
in most case, but was broken by the special case wherein >1 ECS
task shares a listener certificate because both tasks listen
on the same host_header but are differentiated by different
path_pattern values (and different priorities).

This assumption broke for services like authman which meet these
conditions by sharing a host_header. The original design allowed
for creating a listener certificate independently (outside the
apps directory), but naively assumed that separate listener
certificates could be managed in the individual apps subdirectories.

The result is that running the Terraform in *any* of the apps
subdirectories would apply or destroy the listener certificate
regardless of whether other ECS tasks were using it.

In order to fix this proble, we add a manage_listener_certificate
variable. This changes the behavior when applying or destroying
an ECS task.

This variable is only meaningful when a load_balancer object is
defined for the ECS task, and when a public load balancer is used.
Tasks using a private load balancer do not need SSL certificates
because intra-VPC traffic is deemed secure. Tasks not using a
load balancer (such as a daemon process) don't have a listener
at all.

The following assumes that the Terraform for an ECS task specifies a
load_balancer block:

*   If the manage_listener_certificate sub-object in the load_balancer
    block is true (which is the default when a load_balancer block is
    defined), the listener certificate is managed with the ECS task,
    and will have the same lifecycle.

*   If the manage_listener_certificate sub-object in the load_balancer
    block is false, the module assumes that a listener certificate is
    managed independently, in a separate configuration directory,
    using the terraform-aws-lb-listener-certificate module.

In the latter case, the listener certificate in this case is *not*
managed with the container, and persists beyond the lifetime of any
of the individual ECS tasks that use the listener_certificate.

The intent of setting manage_listener_certificate to false is for
use cases where multiple tasks share a host_header, and use
path_pattern and priority sub-objects in the load_balancer block
to distinguish the task to which traffic should be routed.
  • Loading branch information
JonRoma committed Feb 13, 2024
1 parent c35058b commit a126ba2
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 18 deletions.
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

[![Terraform actions status](https://github.com/techservicesillinois/terraform-aws-ecs-service/workflows/terraform/badge.svg)](https://github.com/techservicesillinois/terraform-aws-ecs-service/actions)

Provides a service running under Amazon Elastic Container Service (ECS). An ECS service is essentially a task such as a web service that is expected to run until the task exits. ECS is normally configured to automatically restart a failed task.
Provides a service running under the Amazon Elastic Container Service (ECS). An ECS service is essentially a task such as a web service that is expected to run until terminated. ECS is normally configured to automatically restart a failed task.

ECS allows users to run Docker applications across a cluster of EC2 instances which provide compute power for the workload. Although running Docker containers is itself a straightforward process, configuration of the various infrastructure components (including integrating the containers with an optional application load balancer) is complex.

This module's primary intent is to simplify setting up load-balanced services using a shared
[application load balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html).
The module also supports running tasks in non-load-balanced containers in addition to supporting public and private application load balancers (ALBs).

ECS supports two launch types. The ECS launch type runs containerized services on a customer-managed ECS cluster. Fargate launch type uses an Amazon-managed cluster that allows customers to run containers without having to manage a cluster of their own.
ECS supports two launch types. The ECS launch type runs containerized services on a customer-managed ECS cluster. The Fargate launch type uses an Amazon-managed cluster that allows customers to run containers without having to manage a cluster of their own.

If using a load balancer, the module will create a
[listener rule](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-update-rules.html), a [target group](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html),
Expand Down Expand Up @@ -172,7 +172,7 @@ Health check blocks are documented below.
* `load_balancer` - (Optional) A [load balancer block](#load_balancer).
Load balancer blocks are documented below.

* `name` - (Required) ECS service name. Up to 255 letters (uppercase and lowercase), numbers, hyphens, and underscores are allowed.
* `name` - (Required) ECS service name. Up to 255 letters (uppercase and lowercase), numbers, hyphens, and underscores are allowed.

* `network_configuration` - (Optional) A [network configuration](#network_configuration) block is required for task definitions using the `awsvpc` network mode, in order that those tasks receive an [Elastic Network Interface](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html). The `network_configuration` block is **not**
supported for other network modes.
Expand Down Expand Up @@ -261,7 +261,7 @@ The top-level `autoscale` object consists of three input arguments used to confi

* `metrics` - (Required) A map of [autoscaling metrics](#autoscalemetrics). Each entry in this map consists of a key specifying an autoscaling metric. See [Amazon ECS CloudWatch metrics](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cloudwatch-metrics.html#service_utilization). The value stored under each key in the `metrics` sub-object is itself another sub-object that defines how the specified metric is evaluated by ECS.

> **NOTE:** At this time, this module is limited to the two CloudWatch metrics `CPUUtilization` and `MemoryUtilization` defined in the `AWS/ECS` namespace. As a result, the dimensions `ClusterName` and `ServiceName` associated with that namespace are "hardwired" into this module. This restriction will be eliminated in a future version of this module, although that will require an even deeper data structure.
> **NOTE:** At this time, this module is limited to autoscaling on only the two CloudWatch metrics `CPUUtilization` and `MemoryUtilization` defined in the `AWS/ECS` namespace. As a result, the dimensions `ClusterName` and `ServiceName` associated with that namespace are "hardwired" into this module. This restriction will be eliminated in a future version of this module, although that will require an even deeper data structure.
`autoscale.metrics`
-------------------
Expand All @@ -270,7 +270,7 @@ Each autoscaling `metrics` sub-object allows specifying the following arguments:

* `actions_enabled` - (Optional) Indicates whether or not actions should be executed during any changes to the alarm's state. Defaults to `true`.

* `adjustment_type` - (Required) Whether the adjustment is an absolute number or a percentage of the current capacity.
* `adjustment_type` - (Required) Whether the adjustment is an absolute number or a percentage of the current capacity.

* `cooldown` - (Required) Amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start.

Expand Down Expand Up @@ -352,6 +352,30 @@ A `load_balancer` block may contain the following inputs:
* `host_header` - (Required) A [hostname condition](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#host-conditions)
that defines a rule to forward requests to the service's target group.

* `manage_listener_certificate` - (Optional) This Boolean argument specifies whether a listener certificate should be managed with the ECS task. This is `true` by default. Normally, listener certificates can have the same lifecycle as the ECS task and the listener used by the load balancer to route traffic to the appropriate ECS task.

However, cases exist where more then one ECS task listens on the same `host_header`. These tasks use `path_pattern` and `priority` to distinguish which task handles a particular request. In this case, it is advisable to create a `listener_certificate` in a separate directory, allowing that listener certificate to persist longer than any of the individual ECS tasks. The `manage_listener_certificate` is set to `false` in this case, so that the destruction of one ECS task for maintenance doesn't delete the listener certificate that other tasks depend upon.

In these cases, use the [terraform-aws-lb-listener-certificate](https://github.com/techservicesillinois/terraform-aws-lb-listener-certificate) module in a separate directory to maintain that listener certificate independently of the tasks that use that listener certificate.

For example:

```
./
├── acm/
│ └── terragrunt.hcl
├── apps/
│ ├── common.tfvars
│ ├── bar/
│ │ ├── containers.json.tftpl
│ │ └── terragrunt.hcl
│ └── foo/
│ ├── containers.json.tftpl
│ └── terragrunt.hcl
└── listener-cert/
└── terragrunt.hcl
```

* `name` - (Required) The name of the load balancer.

* `path_pattern` - (Optional) A [path condition](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#path-conditions)
Expand Down Expand Up @@ -532,7 +556,7 @@ This block itself is optional. However, if the block is defined by the caller, *
If the `task_definition` block is defined, and its `template_variables` block is populated, this module runs the Terraform [`templatefile()`](https://developer.hashicorp.com/terraform/language/functions/templatefile) function on the file named in the `container_definition_file` argument. By default, the file name is `containers.json.tftmpl`, but it can be overriden by the user.
The output from the template's rendering is passed to the task definition.

The use of template variables helps make the Terraform configuration DRY by eliminating the need for manual editing – such as during the promotion of services from test to production accounts.
The use of template variables helps make the Terraform configuration DRY by eliminating the need for manual editing – such as during the promotion of services from test to production accounts.
The example below shows how template variables `docker_tag`, `region`, and `registry_id` are passed to the task definition when template rendering is requested by the caller using the `template_variables` block and an appropriately-configured `containers.json.tftmpl` file.

### A `containers.json.tftmpl` file supports template rendering
Expand Down
31 changes: 29 additions & 2 deletions certificate.tf
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
# The value of local.manage_listener_certificate is null if no
# load_balancer block is defined, or no load balancer certificate
# domain is defined. This covers daemon-like tasks that don't
# use a load balancer, as well as tasks that have listeners
# on private load balancers (which do not use certificates because
# intra-VPC traffic is deemed secure, and takes place over port 80).
#
# Otherwise, the local.manage_listener_certificate takes on the
# value of the manage_listener_certificate's load_balancer block.
# The default is true, meaning that a listener certificate is
# managed along with the ECS task and has the same lifecycle.
#
# Set manage_listener_certificate to false when more than one ECS
# task uses the same host_header (which implies that > 1 ECS tasks
# share the listener certificate). In this case, the listener
# certificate should *NOT* be applied and destroyed with any
# particular ECS task.
#
# Use the terraform-aws-lb-listener-certificate module in a separate
# configuration directory to allow the listener certificate to persist
# independently of the state of any individual ECS tasks sharing the
# listener certificate.

locals {
manage_listener_certificate = try(var.load_balancer != null && var.load_balancer.certificate_domain != null && var.load_balancer.manage_listener_certificate, null)
}

data "aws_acm_certificate" "default" {
count = try(var.load_balancer.certificate_domain != null, false) ? 1 : 0
count = try(local.manage_listener_certificate == true, false) ? 1 : 0
domain = var.load_balancer.certificate_domain
statuses = ["ISSUED"]
}

resource "aws_lb_listener_certificate" "default" {
count = try(var.load_balancer.certificate_domain != null, false) ? 1 : 0
count = try(local.manage_listener_certificate == true, false) ? 1 : 0
listener_arn = data.aws_lb_listener.selected[0].arn
certificate_arn = data.aws_acm_certificate.default[0].arn
}
20 changes: 20 additions & 0 deletions outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,23 @@ output "_service_discovery" {
output "_task_definition" {
value = (var._debug) ? var.task_definition : null
}

#output "_aws_acm_certificate_arn" {
# value = try(data.aws_acm_certificate.default[0].arn, null)
#}
#
#output "_aws_acm_certificate_domain" {
# value = try(data.aws_acm_certificate.default[0].domain, null)
#}
#
#output "_aws_lb_listener_port" {
# value = try(data.aws_lb_listener.selected[0].port, null)
#}
#
#output "_aws_lb_listener_protocol" {
# value = try(data.aws_lb_listener.selected[0].protocol, null)
#}
#
#output "_manage_listener_certificate" {
# value = try(local.manage_listener_certificate, "null")
#}
21 changes: 11 additions & 10 deletions variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,17 @@ variable "launch_type" {
variable "load_balancer" {
description = "Load balancer block"
type = object({
certificate_domain = optional(string)
container_name = optional(string)
container_port = optional(number)
deregistration_delay = optional(number)
host_header = optional(string)
name = optional(string)
path_pattern = optional(string, "*")
port = optional(number, 443)
priority = optional(number)
security_group_id = optional(string)
certificate_domain = optional(string)
container_name = optional(string)
container_port = optional(number)
deregistration_delay = optional(number)
host_header = optional(string)
manage_listener_certificate = optional(bool, true)
name = optional(string)
path_pattern = optional(string, "*")
port = optional(number, 443)
priority = optional(number)
security_group_id = optional(string)
})
default = null

Expand Down

0 comments on commit a126ba2

Please sign in to comment.