From 0684c8cd7974bc198a13621fcf6f0e44811874a2 Mon Sep 17 00:00:00 2001 From: Alex Kaplan Date: Wed, 15 May 2024 17:07:57 -0700 Subject: [PATCH] Add ECS Service Connect TLS and timeout (#235) * feat: support ECS service connect TLS * feat: support ECS service connect timeout --------- Co-authored-by: Alex Kaplan --- README.md | 6 ++- docs/terraform.md | 6 ++- main.tf | 124 ++++++++++++++++++++++++++++++++++++++++++---- variables.tf | 11 ++++ 4 files changed, 136 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 11683ac6..acb571da 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,7 @@ Available targets: | Name | Source | Version | |------|--------|---------| | [exec\_label](#module\_exec\_label) | cloudposse/label/null | 0.25.0 | +| [service\_connect\_label](#module\_service\_connect\_label) | cloudposse/label/null | 0.25.0 | | [service\_label](#module\_service\_label) | cloudposse/label/null | 0.25.0 | | [task\_label](#module\_task\_label) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -241,11 +242,13 @@ Available targets: | [aws_ecs_task_definition.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) | resource | | [aws_iam_role.ecs_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.ecs_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.ecs_service_connect_tls](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.ecs_task](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy.ecs_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.ecs_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.ecs_ssm_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy_attachment.ecs_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.ecs_service_connect_tls](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.ecs_task](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_security_group.ecs_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group_rule.alb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | @@ -254,6 +257,7 @@ Available targets: | [aws_security_group_rule.nlb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_iam_policy_document.ecs_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.ecs_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.ecs_service_connect_tls](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.ecs_service_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.ecs_ssm_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.ecs_task](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -323,7 +327,7 @@ Available targets: | [security\_group\_description](#input\_security\_group\_description) | The description to assign to the service security group.
Warning: Changing the description causes the security group to be replaced. | `string` | `"Allow ALL egress from ECS service"` | no | | [security\_group\_enabled](#input\_security\_group\_enabled) | Whether to create a security group for the service. | `bool` | `true` | no | | [security\_group\_ids](#input\_security\_group\_ids) | Security group IDs to allow in Service `network_configuration` if `var.network_mode = "awsvpc"` | `list(string)` | `[]` | no | -| [service\_connect\_configurations](#input\_service\_connect\_configurations) | The list of Service Connect configurations.
See `service_connect_configuration` docs https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#service_connect_configuration |
list(object({
enabled = bool
namespace = optional(string, null)
log_configuration = optional(object({
log_driver = string
options = optional(map(string), null)
secret_option = optional(list(object({
name = string
value_from = string
})), [])
}), null)
service = optional(list(object({
client_alias = list(object({
dns_name = string
port = number
}))
discovery_name = optional(string, null)
ingress_port_override = optional(number, null)
port_name = string
})), [])
}))
| `[]` | no | +| [service\_connect\_configurations](#input\_service\_connect\_configurations) | The list of Service Connect configurations.
See `service_connect_configuration` docs https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#service_connect_configuration |
list(object({
enabled = bool
namespace = optional(string, null)
log_configuration = optional(object({
log_driver = string
options = optional(map(string), null)
secret_option = optional(list(object({
name = string
value_from = string
})), [])
}), null)
service = optional(list(object({
client_alias = list(object({
dns_name = string
port = number
}))
timeout = optional(list(object({
idle_timeout_seconds = optional(number, null)
per_request_timeout_seconds = optional(number, null)
})), [])
tls = optional(list(object({
kms_key = optional(string, null)
role_arn = optional(string, null)
issuer_cert_authority = object({
aws_pca_authority_arn = string
})
})), [])
discovery_name = optional(string, null)
ingress_port_override = optional(number, null)
port_name = string
})), [])
}))
| `[]` | no | | [service\_placement\_constraints](#input\_service\_placement\_constraints) | The rules that are taken into consideration during task placement. Maximum number of placement\_constraints is 10. See [`placement_constraints`](https://www.terraform.io/docs/providers/aws/r/ecs_service.html#placement_constraints-1) docs |
list(object({
type = string
expression = string
}))
| `[]` | no | | [service\_registries](#input\_service\_registries) | Zero or one service discovery registries for the service.
The currently supported service registry is Amazon Route 53 Auto Naming Service - `aws_service_discovery_service`;
see `service_registries` docs https://www.terraform.io/docs/providers/aws/r/ecs_service.html#service_registries-1"
Service registry is object with required key `registry_arn = string` and optional keys
`port = number`
`container_name = string`
`container_port = number` | `list(any)` | `[]` | no | | [service\_role\_arn](#input\_service\_role\_arn) | ARN of the IAM role that allows Amazon ECS to make calls to your load balancer on your behalf. This parameter is required if you are using a load balancer with your service, but only if your task definition does not use the awsvpc network mode. If using awsvpc network mode, do not specify this role. If your account has already created the Amazon ECS service-linked role, that role is used by default for your service unless you specify a role here. | `string` | `null` | no | diff --git a/docs/terraform.md b/docs/terraform.md index 7dd25edd..be2747b9 100644 --- a/docs/terraform.md +++ b/docs/terraform.md @@ -17,6 +17,7 @@ | Name | Source | Version | |------|--------|---------| | [exec\_label](#module\_exec\_label) | cloudposse/label/null | 0.25.0 | +| [service\_connect\_label](#module\_service\_connect\_label) | cloudposse/label/null | 0.25.0 | | [service\_label](#module\_service\_label) | cloudposse/label/null | 0.25.0 | | [task\_label](#module\_task\_label) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -32,11 +33,13 @@ | [aws_ecs_task_definition.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) | resource | | [aws_iam_role.ecs_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.ecs_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.ecs_service_connect_tls](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.ecs_task](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy.ecs_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.ecs_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.ecs_ssm_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy_attachment.ecs_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.ecs_service_connect_tls](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.ecs_task](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_security_group.ecs_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group_rule.alb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | @@ -45,6 +48,7 @@ | [aws_security_group_rule.nlb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_iam_policy_document.ecs_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.ecs_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.ecs_service_connect_tls](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.ecs_service_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.ecs_ssm_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.ecs_task](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -114,7 +118,7 @@ | [security\_group\_description](#input\_security\_group\_description) | The description to assign to the service security group.
Warning: Changing the description causes the security group to be replaced. | `string` | `"Allow ALL egress from ECS service"` | no | | [security\_group\_enabled](#input\_security\_group\_enabled) | Whether to create a security group for the service. | `bool` | `true` | no | | [security\_group\_ids](#input\_security\_group\_ids) | Security group IDs to allow in Service `network_configuration` if `var.network_mode = "awsvpc"` | `list(string)` | `[]` | no | -| [service\_connect\_configurations](#input\_service\_connect\_configurations) | The list of Service Connect configurations.
See `service_connect_configuration` docs https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#service_connect_configuration |
list(object({
enabled = bool
namespace = optional(string, null)
log_configuration = optional(object({
log_driver = string
options = optional(map(string), null)
secret_option = optional(list(object({
name = string
value_from = string
})), [])
}), null)
service = optional(list(object({
client_alias = list(object({
dns_name = string
port = number
}))
discovery_name = optional(string, null)
ingress_port_override = optional(number, null)
port_name = string
})), [])
}))
| `[]` | no | +| [service\_connect\_configurations](#input\_service\_connect\_configurations) | The list of Service Connect configurations.
See `service_connect_configuration` docs https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#service_connect_configuration |
list(object({
enabled = bool
namespace = optional(string, null)
log_configuration = optional(object({
log_driver = string
options = optional(map(string), null)
secret_option = optional(list(object({
name = string
value_from = string
})), [])
}), null)
service = optional(list(object({
client_alias = list(object({
dns_name = string
port = number
}))
timeout = optional(list(object({
idle_timeout_seconds = optional(number, null)
per_request_timeout_seconds = optional(number, null)
})), [])
tls = optional(list(object({
kms_key = optional(string, null)
role_arn = optional(string, null)
issuer_cert_authority = object({
aws_pca_authority_arn = string
})
})), [])
discovery_name = optional(string, null)
ingress_port_override = optional(number, null)
port_name = string
})), [])
}))
| `[]` | no | | [service\_placement\_constraints](#input\_service\_placement\_constraints) | The rules that are taken into consideration during task placement. Maximum number of placement\_constraints is 10. See [`placement_constraints`](https://www.terraform.io/docs/providers/aws/r/ecs_service.html#placement_constraints-1) docs |
list(object({
type = string
expression = string
}))
| `[]` | no | | [service\_registries](#input\_service\_registries) | Zero or one service discovery registries for the service.
The currently supported service registry is Amazon Route 53 Auto Naming Service - `aws_service_discovery_service`;
see `service_registries` docs https://www.terraform.io/docs/providers/aws/r/ecs_service.html#service_registries-1"
Service registry is object with required key `registry_arn = string` and optional keys
`port = number`
`container_name = string`
`container_port = number` | `list(any)` | `[]` | no | | [service\_role\_arn](#input\_service\_role\_arn) | ARN of the IAM role that allows Amazon ECS to make calls to your load balancer on your behalf. This parameter is required if you are using a load balancer with your service, but only if your task definition does not use the awsvpc network mode. If using awsvpc network mode, do not specify this role. If your account has already created the Amazon ECS service-linked role, that role is used by default for your service unless you specify a role here. | `string` | `null` | no | diff --git a/main.tf b/main.tf index ce616aac..0cd1864b 100644 --- a/main.tf +++ b/main.tf @@ -1,13 +1,14 @@ locals { - enabled = module.this.enabled - ecs_service_enabled = local.enabled && var.ecs_service_enabled - task_role_arn = try(var.task_role_arn[0], tostring(var.task_role_arn), "") - create_task_role = local.enabled && length(var.task_role_arn) == 0 - task_exec_role_arn = try(var.task_exec_role_arn[0], tostring(var.task_exec_role_arn), "") - create_exec_role = local.enabled && length(var.task_exec_role_arn) == 0 - enable_ecs_service_role = module.this.enabled && var.network_mode != "awsvpc" && length(var.ecs_load_balancers) >= 1 - create_security_group = local.enabled && var.network_mode == "awsvpc" && var.security_group_enabled - create_task_definition = local.enabled && length(var.task_definition) == 0 + enabled = module.this.enabled + ecs_service_enabled = local.enabled && var.ecs_service_enabled + task_role_arn = try(var.task_role_arn[0], tostring(var.task_role_arn), "") + create_task_role = local.enabled && length(var.task_role_arn) == 0 + task_exec_role_arn = try(var.task_exec_role_arn[0], tostring(var.task_exec_role_arn), "") + create_exec_role = local.enabled && length(var.task_exec_role_arn) == 0 + enable_ecs_service_role = module.this.enabled && var.network_mode != "awsvpc" && length(var.ecs_load_balancers) >= 1 + create_service_connect_tls_role = local.enabled && length(flatten(flatten(var.service_connect_configurations[*].service[*].tls[*]))) > 0 && length(compact(flatten(flatten(var.service_connect_configurations[*].service[*].tls[*].role_arn)))) == 0 + create_security_group = local.enabled && var.network_mode == "awsvpc" && var.security_group_enabled + create_task_definition = local.enabled && length(var.task_definition) == 0 volumes = concat(var.docker_volumes, var.efs_volumes, var.fsx_volumes, var.bind_mount_volumes) @@ -46,6 +47,15 @@ module "exec_label" { context = module.this.context } +module "service_connect_label" { + source = "cloudposse/label/null" + version = "0.25.0" + enabled = local.create_service_connect_tls_role + attributes = ["service-connect-tls"] + + context = module.this.context +} + resource "aws_ecs_task_definition" "default" { count = local.create_task_definition ? 1 : 0 family = module.this.id @@ -302,6 +312,34 @@ resource "aws_iam_role_policy_attachment" "ecs_exec" { role = one(aws_iam_role.ecs_exec[*]["id"]) } +# IAM role that Amazon ECS uses to enable TLS on Service Connect +data "aws_iam_policy_document" "ecs_service_connect_tls" { + count = local.create_service_connect_tls_role ? 1 : 0 + + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "ecs_service_connect_tls" { + count = local.create_service_connect_tls_role ? 1 : 0 + name = module.service_connect_label.id + assume_role_policy = one(data.aws_iam_policy_document.ecs_service_connect_tls[*]["json"]) + permissions_boundary = var.permissions_boundary == "" ? null : var.permissions_boundary + tags = var.role_tags_enabled ? module.service_connect_label.tags : null +} + +resource "aws_iam_role_policy_attachment" "ecs_service_connect_tls" { + count = local.create_service_connect_tls_role ? 1 : 0 + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSInfrastructureRolePolicyForServiceConnectTransportLayerSecurity" + role = one(aws_iam_role.ecs_service_connect_tls[*]["id"]) +} + # Service ## Security Groups resource "aws_security_group" "ecs_service" { @@ -428,6 +466,23 @@ resource "aws_ecs_service" "ignore_changes_task_definition" { port = client_alias.value.port } } + dynamic "timeout" { + for_each = length(service.value.timeout) == 0 ? [] : service.value.timeout + content { + idle_timeout_seconds = timeout.value.idle_timeout_seconds + per_request_timeout_seconds = timeout.value.per_request_timeout_seconds + } + } + dynamic "tls" { + for_each = length(service.value.tls) == 0 ? [] : service.value.tls + content { + kms_key = tls.value.kms_key + role_arn = tls.value.role_arn != null ? tls.value.role_arn : one(aws_iam_role.ecs_service_connect_tls[*].arn) + issuer_cert_authority { + aws_pca_authority_arn = tls.value.issuer_cert_authority.aws_pca_authority_arn + } + } + } } } } @@ -564,6 +619,23 @@ resource "aws_ecs_service" "ignore_changes_task_definition_and_desired_count" { port = client_alias.value.port } } + dynamic "timeout" { + for_each = length(service.value.timeout) == 0 ? [] : service.value.timeout + content { + idle_timeout_seconds = timeout.value.idle_timeout_seconds + per_request_timeout_seconds = timeout.value.per_request_timeout_seconds + } + } + dynamic "tls" { + for_each = length(service.value.tls) == 0 ? [] : service.value.tls + content { + kms_key = tls.value.kms_key + role_arn = tls.value.role_arn != null ? tls.value.role_arn : one(aws_iam_role.ecs_service_connect_tls[*].arn) + issuer_cert_authority { + aws_pca_authority_arn = tls.value.issuer_cert_authority.aws_pca_authority_arn + } + } + } } } } @@ -700,6 +772,23 @@ resource "aws_ecs_service" "ignore_changes_desired_count" { port = client_alias.value.port } } + dynamic "timeout" { + for_each = length(service.value.timeout) == 0 ? [] : service.value.timeout + content { + idle_timeout_seconds = timeout.value.idle_timeout_seconds + per_request_timeout_seconds = timeout.value.per_request_timeout_seconds + } + } + dynamic "tls" { + for_each = length(service.value.tls) == 0 ? [] : service.value.tls + content { + kms_key = tls.value.kms_key + role_arn = tls.value.role_arn != null ? tls.value.role_arn : one(aws_iam_role.ecs_service_connect_tls[*].arn) + issuer_cert_authority { + aws_pca_authority_arn = tls.value.issuer_cert_authority.aws_pca_authority_arn + } + } + } } } } @@ -836,6 +925,23 @@ resource "aws_ecs_service" "default" { port = client_alias.value.port } } + dynamic "timeout" { + for_each = length(service.value.timeout) == 0 ? [] : service.value.timeout + content { + idle_timeout_seconds = timeout.value.idle_timeout_seconds + per_request_timeout_seconds = timeout.value.per_request_timeout_seconds + } + } + dynamic "tls" { + for_each = length(service.value.tls) == 0 ? [] : service.value.tls + content { + kms_key = tls.value.kms_key + role_arn = tls.value.role_arn != null ? tls.value.role_arn : one(aws_iam_role.ecs_service_connect_tls[*].arn) + issuer_cert_authority { + aws_pca_authority_arn = tls.value.issuer_cert_authority.aws_pca_authority_arn + } + } + } } } } diff --git a/variables.tf b/variables.tf index 7080b4bf..4d8f3abe 100644 --- a/variables.tf +++ b/variables.tf @@ -436,6 +436,17 @@ variable "service_connect_configurations" { dns_name = string port = number })) + timeout = optional(list(object({ + idle_timeout_seconds = optional(number, null) + per_request_timeout_seconds = optional(number, null) + })), []) + tls = optional(list(object({ + kms_key = optional(string, null) + role_arn = optional(string, null) + issuer_cert_authority = object({ + aws_pca_authority_arn = string + }) + })), []) discovery_name = optional(string, null) ingress_port_override = optional(number, null) port_name = string