From ab0da5e7f20022c301f934a648859d0fd347659c Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Fri, 19 Jun 2026 09:20:27 +0100 Subject: [PATCH 01/22] feat(ecs-service): add bare bones service --- .github/dependabot.yaml | 1 + .../modules/ecs-service/.terraform.lock.hcl | 30 ++++++++++++++++ infrastructure/modules/ecs-service/README.md | 36 +++++++++++++++++++ infrastructure/modules/ecs-service/main.tf | 4 +++ infrastructure/modules/ecs-service/outputs.tf | 1 + .../modules/ecs-service/variables.tf | 1 + .../modules/ecs-service/versions.tf | 10 ++++++ 7 files changed, 83 insertions(+) create mode 100644 infrastructure/modules/ecs-service/.terraform.lock.hcl create mode 100644 infrastructure/modules/ecs-service/README.md create mode 100644 infrastructure/modules/ecs-service/main.tf create mode 100644 infrastructure/modules/ecs-service/outputs.tf create mode 100644 infrastructure/modules/ecs-service/variables.tf create mode 100644 infrastructure/modules/ecs-service/versions.tf diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 229165e4..fc0bf145 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -44,6 +44,7 @@ updates: - "infrastructure/modules/cw-firehose-splunk" - "infrastructure/modules/ecr" - "infrastructure/modules/ecs-cluster" + - "infrastructure/modules/ecs-service" - "infrastructure/modules/elasticache" - "infrastructure/modules/github-config" - "infrastructure/modules/guardduty" diff --git a/infrastructure/modules/ecs-service/.terraform.lock.hcl b/infrastructure/modules/ecs-service/.terraform.lock.hcl new file mode 100644 index 00000000..eb4f3fd1 --- /dev/null +++ b/infrastructure/modules/ecs-service/.terraform.lock.hcl @@ -0,0 +1,30 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.51.0" + constraints = ">= 6.34.0" + hashes = [ + "h1:017ISHZZBI+yeqA4AAtgLQJC7Lhd4wYM7tEKYmlk/7Y=", + "h1:4c8zjgtGH0QgP+p/cF1UqdqkvD7V5i0ZxqslieZLTbc=", + "h1:QWxF+1ePJ4qFCHEc6PyHNeXc865wLvrWVl71d/nABa8=", + "h1:aPBmqoiYqfrIgCGwzuemljkOXuGCYQRTXo91nQxrE+s=", + "h1:bclp+xS1fYeOCil0XZO6mKvEeHFESt5K/XotVSZND54=", + "zh:03fcea0a1ea2ca81d62d4d2e2961181bef9068b1c701f2cddc4aa5fac105818a", + "zh:1213944cd623143974ea5c9b70b22ae1ccca33d743924c149ed089d34b8e08b4", + "zh:190a46da0c69082b74da48238ce134d2fc9893e09122ac249c5689f88eab7e13", + "zh:1b312a4b53fa3cf731f95e674c033865feea5455f163b86136f2614424637293", + "zh:2b319814806222c5aba196b1a78756a6b36dc5c91f85edda349234d8a2f20a6a", + "zh:2bddf92c8efc6ad445a2eb8a0e5f88742a0596392c3a4ebc350ebb4105a4a96d", + "zh:3bef0c4f675c09034ff017cf899977b1765b2c0b3d1e489bcb06a5fcac316e2d", + "zh:47c46b5aa22199638fed5c93b195bbfd1182a1408edad4e5c39d4a73a04493f6", + "zh:5f808699650f6db961964466c77f5a581eab142a91c2e54810bb09b6f2fcd3f2", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:ada97e6be10164f452e278c23412b8597698a9c95ffb68fe83629d63d85906f3", + "zh:c4d73a91810d8dbcf9abbd431d41fcceebb48f8b6fd3c28a84bb3c6ed08be2e9", + "zh:c63ec875d38fc557b16b0b2b0ab1c7635852799453113240e21a52409de94a71", + "zh:cdd0209a755fc3aa14855aa013dae4b166a2fc7f6d3cbb673f7ff2142f5b63a2", + "zh:e5e665a27290391fd1bffc093ab68b596f6c507785be2e3f0949fab4fd6aec1b", + "zh:f6c42046a31d65eff2793737656b38931f90318b53661046bb84326cd4cb558f", + ] +} diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md new file mode 100644 index 00000000..77bae952 --- /dev/null +++ b/infrastructure/modules/ecs-service/README.md @@ -0,0 +1,36 @@ +# DAVEH + + + + +## Requirements + +| Name | Version | +| ---- | ------- | +| [terraform](#requirement\_terraform) | >= 1.13 | +| [aws](#requirement\_aws) | >= 6.34 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +| ---- | ------ | ------- | +| [ecs\_service](#module\_ecs\_service) | terraform-aws-modules/ecs/aws//modules/service | ~> 7.5.0 | + +## Resources + +No resources. + +## Inputs + +No inputs. + +## Outputs + +No outputs. + + + diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf new file mode 100644 index 00000000..359595df --- /dev/null +++ b/infrastructure/modules/ecs-service/main.tf @@ -0,0 +1,4 @@ +module "ecs_service" { + source = "terraform-aws-modules/ecs/aws//modules/service" + version = "~> 7.5.0" +} diff --git a/infrastructure/modules/ecs-service/outputs.tf b/infrastructure/modules/ecs-service/outputs.tf new file mode 100644 index 00000000..c9fb5240 --- /dev/null +++ b/infrastructure/modules/ecs-service/outputs.tf @@ -0,0 +1 @@ +# DAVEH diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf new file mode 100644 index 00000000..c9fb5240 --- /dev/null +++ b/infrastructure/modules/ecs-service/variables.tf @@ -0,0 +1 @@ +# DAVEH diff --git a/infrastructure/modules/ecs-service/versions.tf b/infrastructure/modules/ecs-service/versions.tf new file mode 100644 index 00000000..542c57d5 --- /dev/null +++ b/infrastructure/modules/ecs-service/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.13" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.34" + } + } +} From 82d768195c2b97e8186334c113c9d108803ab319 Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 08:20:32 +0100 Subject: [PATCH 02/22] feat(ecs-service): add context --- infrastructure/modules/ecs-service/README.md | 35 +- infrastructure/modules/ecs-service/context.tf | 376 ++++++++++++++++++ infrastructure/modules/ecs-service/main.tf | 4 + 3 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 infrastructure/modules/ecs-service/context.tf diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index 77bae952..d5aaab0e 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -19,6 +19,7 @@ No providers. | Name | Source | Version | | ---- | ------ | ------- | | [ecs\_service](#module\_ecs\_service) | terraform-aws-modules/ecs/aws//modules/service | ~> 7.5.0 | +| [this](#module\_this) | ../tags | n/a | ## Resources @@ -26,7 +27,39 @@ No resources. ## Inputs -No inputs. +| Name | Description | Type | Default | Required | +| ---- | ----------- | ---- | ------- | :------: | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [application\_role](#input\_application\_role) | The role the application is performing | `string` | `"General"` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [aws\_region](#input\_aws\_region) | The AWS region | `string` | `"eu-west-2"` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"project": null,
"regex_replace_chars": null,
"region": null,
"service": null,
"stack": null,
"tags": {},
"terraform_source": null,
"workspace": null
}
| no | +| [data\_classification](#input\_data\_classification) | Used to identify the data classification of the resource, e.g 1-5 | `string` | `"n/a"` | no | +| [data\_type](#input\_data\_type) | The tag data\_type | `string` | `"None"` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used to indicate role, e.g. 'prd', 'dev', 'test', 'preprod', 'prod', 'uat' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [on\_off\_pattern](#input\_on\_off\_pattern) | Used to turn resources on and off based on a time pattern | `string` | `"n/a"` | no | +| [owner](#input\_owner) | The name and or NHS.net email address of the service owner | `string` | `"None"` | no | +| [project](#input\_project) | ID element. A project identifier, indicating the name or role of the project the resource is for, such as `website` or `api` | `string` | `null` | no | +| [public\_facing](#input\_public\_facing) | Whether this resource is public facing | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | ID element \_(Rarely used, not included by default)\_. Usually an abbreviation of the selected AWS region e.g. 'uw2', 'ew2' or 'gbl' for resources like IAM roles that have no region | `string` | `null` | no | +| [service](#input\_service) | ID element. Usually an abbreviation of your service directorate name, e.g. 'bcss' or 'csms', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [service\_category](#input\_service\_category) | The tag service\_category | `string` | `"n/a"` | no | +| [stack](#input\_stack) | ID element. The name of the stack/component, e.g. `database`, `web`, `waf`, `eks` | `string` | `null` | no | +| [tag\_version](#input\_tag\_version) | Used to identify the tagging version in use | `string` | `"1.0"` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [terraform\_source](#input\_terraform\_source) | Source location to record in the Terraform\_source tag. Defaults to this module path. | `string` | `null` | no | +| [tool](#input\_tool) | The tool used to deploy the resource | `string` | `"Terraform"` | no | +| [workspace](#input\_workspace) | ID element. The Terraform workspace, to help ensure generated IDs are unique across workspaces | `string` | `null` | no | ## Outputs diff --git a/infrastructure/modules/ecs-service/context.tf b/infrastructure/modules/ecs-service/context.tf new file mode 100644 index 00000000..62befcb0 --- /dev/null +++ b/infrastructure/modules/ecs-service/context.tf @@ -0,0 +1,376 @@ +# tflint-ignore-file: terraform_standard_module_structure, terraform_unused_declarations +# +# ONLY EDIT THIS FILE IN github.com/NHSDigital/screening-terraform-modules-aws/infrastructure/modules/tags +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/NHSDigital/screening-terraform-modules-aws/blob/master/infrastructure/modules/tags/exports/context.tf +# and then place it in your Terraform module to automatically get +# tag module standard configuration inputs suitable for passing +# to other modules. +# +# curl -sL https://raw.githubusercontent.com/NHSDigital/screening-terraform-modules-aws/master/infrastructure/modules/tags/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "../tags" + + enabled = var.enabled + service = var.service + project = var.project + region = var.region + environment = var.environment + stack = var.stack + workspace = var.workspace + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + terraform_source = coalesce(var.terraform_source, path.module) + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of screening-terraform-modules-aws/tags/variables.tf here +# tflint-ignore: terraform_unused_declarations +variable "aws_region" { + type = string + description = "The AWS region" + default = "eu-west-2" + validation { + condition = contains(["eu-west-1", "eu-west-2", "us-east-1"], var.aws_region) + error_message = "AWS Region must be one of eu-west-1, eu-west-2, us-east-1" + } +} + +variable "context" { + type = any + default = { + enabled = true + service = null + project = null + region = null + environment = null + stack = null + workspace = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + terraform_source = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "terraform_source" { + type = string + default = null + description = "Source location to record in the Terraform_source tag. Defaults to this module path." +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "service" { + type = string + default = null + description = "ID element. Usually an abbreviation of your service directorate name, e.g. 'bcss' or 'csms', to help ensure generated IDs are globally unique" +} + +variable "region" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. Usually an abbreviation of the selected AWS region e.g. 'uw2', 'ew2' or 'gbl' for resources like IAM roles that have no region" +} + +variable "project" { + type = string + default = null + description = "ID element. A project identifier, indicating the name or role of the project the resource is for, such as `website` or `api`" +} +variable "stack" { + type = string + default = null + description = "ID element. The name of the stack/component, e.g. `database`, `web`, `waf`, `eks`" +} +variable "workspace" { + type = string + default = null + description = "ID element. The Terraform workspace, to help ensure generated IDs are unique across workspaces" +} +variable "environment" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prd', 'dev', 'test', 'preprod', 'prod', 'uat'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +variable "owner" { + type = string + description = "The name and or NHS.net email address of the service owner" + default = "None" +} + +variable "tag_version" { + type = string + description = "Used to identify the tagging version in use" + default = "1.0" +} + +variable "data_classification" { + type = string + description = "Used to identify the data classification of the resource, e.g 1-5" + default = "n/a" + validation { + condition = contains(["n/a", "1", "2", "3", "4", "5"], var.data_classification) + error_message = "Data Classification must be \"n/a\" or between 1-5" + } +} + +variable "data_type" { + type = string + description = "The tag data_type" + default = "None" + validation { + condition = contains(["None", "PCD", "PID", "Anonymised", "UserAccount", "Audit"], var.data_type) + error_message = "Data Type must be one of None, PCD, PID, Anonymised, UserAccount, Audit" + } +} + + +variable "public_facing" { + type = bool + description = "Whether this resource is public facing" + default = false +} + +variable "service_category" { + type = string + description = "The tag service_category" + default = "n/a" + validation { + condition = contains(["n/a", "Bronze", "Silver", "Gold", "Platinum"], var.service_category) + error_message = "The Service Category must be one of n/a, Bronze, Silver, Gold, Platinum" + } +} +variable "on_off_pattern" { + type = string + description = "Used to turn resources on and off based on a time pattern" + default = "n/a" +} + +variable "application_role" { + type = string + description = "The role the application is performing" + default = "General" +} + +variable "tool" { + type = string + description = "The tool used to deploy the resource" + default = "Terraform" +} + +#### End of copy of screening-terraform-modules-aws/tags/variables.tf diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index 359595df..0fc4ded1 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -1,4 +1,8 @@ module "ecs_service" { source = "terraform-aws-modules/ecs/aws//modules/service" version = "~> 7.5.0" + + create = module.this.enabled + name = module.this.name + tags = module.this.tags } From b8551b25c46f9a062638f5e643453823ced89139 Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 08:41:35 +0100 Subject: [PATCH 03/22] feat(ecs-service): add inputs a --- infrastructure/modules/ecs-service/README.md | 8 + infrastructure/modules/ecs-service/main.tf | 9 + .../modules/ecs-service/variables.tf | 214 +++++++++++++++++- 3 files changed, 230 insertions(+), 1 deletion(-) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index d5aaab0e..033cf591 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -30,8 +30,16 @@ No resources. | Name | Description | Type | Default | Required | | ---- | ----------- | ---- | ------- | :------: | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alarms](#input\_alarms) | Information about the CloudWatch alarms |
object({
alarm_names = list(string)
enable = optional(bool, true)
rollback = optional(bool, true)
})
| `null` | no | | [application\_role](#input\_application\_role) | The role the application is performing | `string` | `"General"` | no | +| [assign\_public\_ip](#input\_assign\_public\_ip) | Assign a public IP address to the ENI (Fargate launch type only) | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [autoscaling\_max\_capacity](#input\_autoscaling\_max\_capacity) | Maximum number of tasks to run in your service | `number` | `10` | no | +| [autoscaling\_min\_capacity](#input\_autoscaling\_min\_capacity) | Minimum number of tasks to run in your service | `number` | `1` | no | +| [autoscaling\_policies](#input\_autoscaling\_policies) | Map of autoscaling policies to create for the service |
map(object({
name = optional(string) # Will fall back to the key name if not provided
policy_type = optional(string, "TargetTrackingScaling")
predictive_scaling_policy_configuration = optional(object({
max_capacity_breach_behavior = optional(string)
max_capacity_buffer = optional(number)
metric_specification = list(object({
customized_capacity_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_load_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
customized_scaling_metric_specification = optional(object({
metric_data_query = list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimension = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
namespace = optional(string)
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
}))
}))
predefined_load_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_metric_pair_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
predefined_scaling_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
target_value = number
}))
mode = optional(string)
scheduling_buffer_time = optional(number)
}))
step_scaling_policy_configuration = optional(object({
adjustment_type = optional(string)
cooldown = optional(number)
metric_aggregation_type = optional(string)
min_adjustment_magnitude = optional(number)
step_adjustment = optional(list(object({
metric_interval_lower_bound = optional(string)
metric_interval_upper_bound = optional(string)
scaling_adjustment = number
})))
}))
target_tracking_scaling_policy_configuration = optional(object({
customized_metric_specification = optional(object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = optional(string)
metrics = optional(list(object({
expression = optional(string)
id = string
label = optional(string)
metric_stat = optional(object({
metric = object({
dimensions = optional(list(object({
name = string
value = string
})))
metric_name = string
namespace = string
})
stat = string
unit = optional(string)
}))
return_data = optional(bool)
})))
namespace = optional(string)
statistic = optional(string)
unit = optional(string)
}))
disable_scale_in = optional(bool)
predefined_metric_specification = optional(object({
predefined_metric_type = string
resource_label = optional(string)
}))
scale_in_cooldown = optional(number, 300)
scale_out_cooldown = optional(number, 60)
target_value = optional(number, 75)
}))
}))
|
{
"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"
}
}
}
}
| no | +| [autoscaling\_scheduled\_actions](#input\_autoscaling\_scheduled\_actions) | Map of autoscaling scheduled actions to create for the service |
map(object({
name = optional(string)
min_capacity = number
max_capacity = number
schedule = string
start_time = optional(string)
end_time = optional(string)
timezone = optional(string)
}))
| `null` | no | +| [autoscaling\_suspended\_state](#input\_autoscaling\_suspended\_state) | Configuration block that specifies whether the scaling activities for the service are in a suspended state |
object({
dynamic_scaling_in_suspended = optional(bool, false)
dynamic_scaling_out_suspended = optional(bool, false)
scheduled_scaling_suspended = optional(bool, false)
})
| `null` | no | +| [availability\_zone\_rebalancing](#input\_availability\_zone\_rebalancing) | ECS automatically redistributes tasks within a service across Availability Zones (AZs) to mitigate the risk of impaired application availability due to underlying infrastructure failures and task lifecycle activities. The valid values are `ENABLED` and `DISABLED`. Defaults to `DISABLED` | `string` | `null` | no | | [aws\_region](#input\_aws\_region) | The AWS region | `string` | `"eu-west-2"` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"project": null,
"regex_replace_chars": null,
"region": null,
"service": null,
"stack": null,
"tags": {},
"terraform_source": null,
"workspace": null
}
| no | | [data\_classification](#input\_data\_classification) | Used to identify the data classification of the resource, e.g 1-5 | `string` | `"n/a"` | no | diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index 0fc4ded1..7cce6d25 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -5,4 +5,13 @@ module "ecs_service" { create = module.this.enabled name = module.this.name tags = module.this.tags + + alarms = var.alarms + assign_public_ip = var.assign_public_ip + autoscaling_max_capacity = var.autoscaling_max_capacity + autoscaling_min_capacity = var.autoscaling_min_capacity + autoscaling_policies = var.autoscaling_policies + autoscaling_scheduled_actions = var.autoscaling_scheduled_actions + autoscaling_suspended_state = var.autoscaling_suspended_state + availability_zone_rebalancing = var.availability_zone_rebalancing } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index c9fb5240..6a09ccee 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -1 +1,213 @@ -# DAVEH +variable "alarms" { + description = "Information about the CloudWatch alarms" + type = object({ + alarm_names = list(string) + enable = optional(bool, true) + rollback = optional(bool, true) + }) + default = null +} + +variable "assign_public_ip" { + description = "Assign a public IP address to the ENI (Fargate launch type only)" + type = bool + default = false +} + +variable "autoscaling_max_capacity" { + description = "Maximum number of tasks to run in your service" + type = number + default = 10 +} + +variable "autoscaling_min_capacity" { + description = "Minimum number of tasks to run in your service" + type = number + default = 1 +} + +variable "autoscaling_policies" { + description = "Map of autoscaling policies to create for the service" + type = map(object({ + name = optional(string) # Will fall back to the key name if not provided + policy_type = optional(string, "TargetTrackingScaling") + predictive_scaling_policy_configuration = optional(object({ + max_capacity_breach_behavior = optional(string) + max_capacity_buffer = optional(number) + metric_specification = list(object({ + customized_capacity_metric_specification = optional(object({ + metric_data_query = list(object({ + expression = optional(string) + id = string + label = optional(string) + metric_stat = optional(object({ + metric = object({ + dimension = optional(list(object({ + name = string + value = string + }))) + metric_name = optional(string) + namespace = optional(string) + }) + stat = string + unit = optional(string) + })) + return_data = optional(bool) + })) + })) + customized_load_metric_specification = optional(object({ + metric_data_query = list(object({ + expression = optional(string) + id = string + label = optional(string) + metric_stat = optional(object({ + metric = object({ + dimension = optional(list(object({ + name = string + value = string + }))) + metric_name = optional(string) + namespace = optional(string) + }) + stat = string + unit = optional(string) + })) + return_data = optional(bool) + })) + })) + customized_scaling_metric_specification = optional(object({ + metric_data_query = list(object({ + expression = optional(string) + id = string + label = optional(string) + metric_stat = optional(object({ + metric = object({ + dimension = optional(list(object({ + name = string + value = string + }))) + metric_name = optional(string) + namespace = optional(string) + }) + stat = string + unit = optional(string) + })) + return_data = optional(bool) + })) + })) + predefined_load_metric_specification = optional(object({ + predefined_metric_type = string + resource_label = optional(string) + })) + predefined_metric_pair_specification = optional(object({ + predefined_metric_type = string + resource_label = optional(string) + })) + predefined_scaling_metric_specification = optional(object({ + predefined_metric_type = string + resource_label = optional(string) + })) + target_value = number + })) + mode = optional(string) + scheduling_buffer_time = optional(number) + })) + step_scaling_policy_configuration = optional(object({ + adjustment_type = optional(string) + cooldown = optional(number) + metric_aggregation_type = optional(string) + min_adjustment_magnitude = optional(number) + step_adjustment = optional(list(object({ + metric_interval_lower_bound = optional(string) + metric_interval_upper_bound = optional(string) + scaling_adjustment = number + }))) + })) + target_tracking_scaling_policy_configuration = optional(object({ + customized_metric_specification = optional(object({ + dimensions = optional(list(object({ + name = string + value = string + }))) + metric_name = optional(string) + metrics = optional(list(object({ + expression = optional(string) + id = string + label = optional(string) + metric_stat = optional(object({ + metric = object({ + dimensions = optional(list(object({ + name = string + value = string + }))) + metric_name = string + namespace = string + }) + stat = string + unit = optional(string) + })) + return_data = optional(bool) + }))) + namespace = optional(string) + statistic = optional(string) + unit = optional(string) + })) + disable_scale_in = optional(bool) + predefined_metric_specification = optional(object({ + predefined_metric_type = string + resource_label = optional(string) + })) + scale_in_cooldown = optional(number, 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" + } + } + } + } +} + +variable "autoscaling_scheduled_actions" { + description = "Map of autoscaling scheduled actions to create for the service" + type = map(object({ + name = optional(string) + min_capacity = number + max_capacity = number + schedule = string + start_time = optional(string) + end_time = optional(string) + timezone = optional(string) + })) + default = null +} + +variable "autoscaling_suspended_state" { + description = "Configuration block that specifies whether the scaling activities for the service are in a suspended state" + type = object({ + dynamic_scaling_in_suspended = optional(bool, false) + dynamic_scaling_out_suspended = optional(bool, false) + scheduled_scaling_suspended = optional(bool, false) + }) + default = null +} + +variable "availability_zone_rebalancing" { + description = "ECS automatically redistributes tasks within a service across Availability Zones (AZs) to mitigate the risk of impaired application availability due to underlying infrastructure failures and task lifecycle activities. The valid values are `ENABLED` and `DISABLED`. Defaults to `DISABLED`" + type = string + default = null +} From 7ca4f72dcb11b0680b706f23e7ba16e904466494 Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 08:55:21 +0100 Subject: [PATCH 04/22] feat(ecs-service): add inputs ca-cp --- infrastructure/modules/ecs-service/README.md | 4 + infrastructure/modules/ecs-service/main.tf | 4 + .../modules/ecs-service/variables.tf | 165 ++++++++++++++++++ 3 files changed, 173 insertions(+) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index 033cf591..49e58db7 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -41,7 +41,11 @@ No resources. | [autoscaling\_suspended\_state](#input\_autoscaling\_suspended\_state) | Configuration block that specifies whether the scaling activities for the service are in a suspended state |
object({
dynamic_scaling_in_suspended = optional(bool, false)
dynamic_scaling_out_suspended = optional(bool, false)
scheduled_scaling_suspended = optional(bool, false)
})
| `null` | no | | [availability\_zone\_rebalancing](#input\_availability\_zone\_rebalancing) | ECS automatically redistributes tasks within a service across Availability Zones (AZs) to mitigate the risk of impaired application availability due to underlying infrastructure failures and task lifecycle activities. The valid values are `ENABLED` and `DISABLED`. Defaults to `DISABLED` | `string` | `null` | no | | [aws\_region](#input\_aws\_region) | The AWS region | `string` | `"eu-west-2"` | no | +| [capacity\_provider\_strategy](#input\_capacity\_provider\_strategy) | Capacity provider strategies to use for the service. Can be one or more |
map(object({
base = optional(number)
capacity_provider = string
weight = optional(number)
}))
| `null` | no | +| [cluster\_arn](#input\_cluster\_arn) | ARN of the ECS cluster where the resources will be provisioned | `string` | `""` | no | +| [container\_definitions](#input\_container\_definitions) | A map of valid container definitions . Please note that you should only provide values that are part of the container definition document |
map(object({
create = optional(bool, true)
operating_system_family = optional(string)
tags = optional(map(string)) # Container definition
command = optional(list(string))
cpu = optional(number)
credentialSpecs = optional(list(string))
dependsOn = optional(list(object({
condition = string
containerName = string
})))
disableNetworking = optional(bool)
dnsSearchDomains = optional(list(string))
dnsServers = optional(list(string))
dockerLabels = optional(map(string))
dockerSecurityOptions = optional(list(string))
# DAVEH: following line was comment to preceeding line
enable_execute_command = optional(bool, false) # Set in standalone variable
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, 30)
retries = optional(number, 3)
startPeriod = optional(number)
timeout = optional(number, 5)
}))
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, 30)
stopTimeout = optional(number, 120)
systemControls = optional(list(object({
namespace = optional(string)
value = optional(string)
})))
ulimits = optional(list(object({
hardLimit = number
name = string
softLimit = number
})))
user = optional(string)
versionConsistency = optional(string)
volumesFrom = optional(list(object({
readOnly = optional(bool)
sourceContainer = optional(string)
})))
workingDirectory = optional(string)
# Cloudwatch Log Group
service = optional(string)
enable_cloudwatch_logging = optional(bool)
create_cloudwatch_log_group = optional(bool)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool)
cloudwatch_log_group_class = optional(string)
cloudwatch_log_group_retention_in_days = optional(number)
cloudwatch_log_group_kms_key_id = optional(string)
}))
| `{}` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"project": null,
"regex_replace_chars": null,
"region": null,
"service": null,
"stack": null,
"tags": {},
"terraform_source": null,
"workspace": null
}
| no | +| [cpu](#input\_cpu) | Number of cpu units used by the task. If the `requires_compatibilities` is `FARGATE` this field is required | `number` | `1024` | no | | [data\_classification](#input\_data\_classification) | Used to identify the data classification of the resource, e.g 1-5 | `string` | `"n/a"` | no | | [data\_type](#input\_data\_type) | The tag data\_type | `string` | `"None"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index 7cce6d25..18ca2eb6 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -14,4 +14,8 @@ module "ecs_service" { autoscaling_scheduled_actions = var.autoscaling_scheduled_actions autoscaling_suspended_state = var.autoscaling_suspended_state availability_zone_rebalancing = var.availability_zone_rebalancing + capacity_provider_strategy = var.capacity_provider_strategy + cluster_arn = var.cluster_arn + container_definitions = var.container_definitions + cpu = var.cpu } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index 6a09ccee..221859a5 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -211,3 +211,168 @@ variable "availability_zone_rebalancing" { type = string default = null } + +variable "capacity_provider_strategy" { + description = "Capacity provider strategies to use for the service. Can be one or more" + type = map(object({ + base = optional(number) + capacity_provider = string + weight = optional(number) + })) + default = null +} + +variable "cluster_arn" { + description = "ARN of the ECS cluster where the resources will be provisioned" + type = string + default = "" +} + +variable "container_definitions" { + description = "A map of valid container definitions . Please note that you should only provide values that are part of the container definition document" + type = map(object({ + create = optional(bool, true) + operating_system_family = optional(string) + tags = optional(map(string)) # Container definition + command = optional(list(string)) + cpu = optional(number) + credentialSpecs = optional(list(string)) + dependsOn = optional(list(object({ + condition = string + containerName = string + }))) + disableNetworking = optional(bool) + dnsSearchDomains = optional(list(string)) + dnsServers = optional(list(string)) + dockerLabels = optional(map(string)) + dockerSecurityOptions = optional(list(string)) + # DAVEH: following line was comment to preceeding line + enable_execute_command = optional(bool, false) # Set in standalone variable + 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, 30) + retries = optional(number, 3) + startPeriod = optional(number) + timeout = optional(number, 5) + })) + 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, 30) + stopTimeout = optional(number, 120) + systemControls = optional(list(object({ + namespace = optional(string) + value = optional(string) + }))) + ulimits = optional(list(object({ + hardLimit = number + name = string + softLimit = number + }))) + user = optional(string) + versionConsistency = optional(string) + volumesFrom = optional(list(object({ + readOnly = optional(bool) + sourceContainer = optional(string) + }))) + workingDirectory = optional(string) + # Cloudwatch Log Group + service = optional(string) + enable_cloudwatch_logging = optional(bool) + create_cloudwatch_log_group = optional(bool) + cloudwatch_log_group_name = optional(string) + cloudwatch_log_group_use_name_prefix = optional(bool) + cloudwatch_log_group_class = optional(string) + cloudwatch_log_group_retention_in_days = optional(number) + cloudwatch_log_group_kms_key_id = optional(string) + })) + default = {} +} + +variable "cpu" { + description = "Number of cpu units used by the task. If the `requires_compatibilities` is `FARGATE` this field is required" + type = number + default = 1024 +} From 05e5cb36eb9a431cbb4df6214e44242e53d098eb Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 09:09:18 +0100 Subject: [PATCH 05/22] feat(ecs-service): add inputs cr --- infrastructure/modules/ecs-service/README.md | 8 ++++ infrastructure/modules/ecs-service/main.tf | 32 ++++++++----- .../modules/ecs-service/variables.tf | 48 +++++++++++++++++++ 3 files changed, 76 insertions(+), 12 deletions(-) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index 49e58db7..29e24624 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -46,6 +46,14 @@ No resources. | [container\_definitions](#input\_container\_definitions) | A map of valid container definitions . Please note that you should only provide values that are part of the container definition document |
map(object({
create = optional(bool, true)
operating_system_family = optional(string)
tags = optional(map(string)) # Container definition
command = optional(list(string))
cpu = optional(number)
credentialSpecs = optional(list(string))
dependsOn = optional(list(object({
condition = string
containerName = string
})))
disableNetworking = optional(bool)
dnsSearchDomains = optional(list(string))
dnsServers = optional(list(string))
dockerLabels = optional(map(string))
dockerSecurityOptions = optional(list(string))
# DAVEH: following line was comment to preceeding line
enable_execute_command = optional(bool, false) # Set in standalone variable
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, 30)
retries = optional(number, 3)
startPeriod = optional(number)
timeout = optional(number, 5)
}))
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, 30)
stopTimeout = optional(number, 120)
systemControls = optional(list(object({
namespace = optional(string)
value = optional(string)
})))
ulimits = optional(list(object({
hardLimit = number
name = string
softLimit = number
})))
user = optional(string)
versionConsistency = optional(string)
volumesFrom = optional(list(object({
readOnly = optional(bool)
sourceContainer = optional(string)
})))
workingDirectory = optional(string)
# Cloudwatch Log Group
service = optional(string)
enable_cloudwatch_logging = optional(bool)
create_cloudwatch_log_group = optional(bool)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool)
cloudwatch_log_group_class = optional(string)
cloudwatch_log_group_retention_in_days = optional(number)
cloudwatch_log_group_kms_key_id = optional(string)
}))
| `{}` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"project": null,
"regex_replace_chars": null,
"region": null,
"service": null,
"stack": null,
"tags": {},
"terraform_source": null,
"workspace": null
}
| no | | [cpu](#input\_cpu) | Number of cpu units used by the task. If the `requires_compatibilities` is `FARGATE` this field is required | `number` | `1024` | no | +| [create\_iam\_role](#input\_create\_iam\_role) | Determines whether the ECS service IAM role should be created | `bool` | `true` | no | +| [create\_infrastructure\_iam\_role](#input\_create\_infrastructure\_iam\_role) | Determines whether the ECS infrastructure IAM role should be created | `bool` | `true` | no | +| [create\_security\_group](#input\_create\_security\_group) | Determines if a security group is created | `bool` | `true` | no | +| [create\_service](#input\_create\_service) | Determines whether service resource will be created (set to `false` in case you want to create task definition only) | `bool` | `true` | no | +| [create\_task\_definition](#input\_create\_task\_definition) | Determines whether to create a task definition or use existing/provided | `bool` | `true` | no | +| [create\_task\_exec\_iam\_role](#input\_create\_task\_exec\_iam\_role) | Determines whether the ECS task definition IAM role should be created | `bool` | `true` | no | +| [create\_task\_exec\_policy](#input\_create\_task\_exec\_policy) | Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters | `bool` | `true` | no | +| [create\_tasks\_iam\_role](#input\_create\_tasks\_iam\_role) | Determines whether the ECS tasks IAM role should be created | `bool` | `true` | no | | [data\_classification](#input\_data\_classification) | Used to identify the data classification of the resource, e.g 1-5 | `string` | `"n/a"` | no | | [data\_type](#input\_data\_type) | The tag data\_type | `string` | `"None"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index 18ca2eb6..1ca8f043 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -6,16 +6,24 @@ module "ecs_service" { name = module.this.name tags = module.this.tags - alarms = var.alarms - assign_public_ip = var.assign_public_ip - autoscaling_max_capacity = var.autoscaling_max_capacity - autoscaling_min_capacity = var.autoscaling_min_capacity - autoscaling_policies = var.autoscaling_policies - autoscaling_scheduled_actions = var.autoscaling_scheduled_actions - autoscaling_suspended_state = var.autoscaling_suspended_state - availability_zone_rebalancing = var.availability_zone_rebalancing - capacity_provider_strategy = var.capacity_provider_strategy - cluster_arn = var.cluster_arn - container_definitions = var.container_definitions - cpu = var.cpu + alarms = var.alarms + assign_public_ip = var.assign_public_ip + autoscaling_max_capacity = var.autoscaling_max_capacity + autoscaling_min_capacity = var.autoscaling_min_capacity + autoscaling_policies = var.autoscaling_policies + autoscaling_scheduled_actions = var.autoscaling_scheduled_actions + autoscaling_suspended_state = var.autoscaling_suspended_state + availability_zone_rebalancing = var.availability_zone_rebalancing + capacity_provider_strategy = var.capacity_provider_strategy + cluster_arn = var.cluster_arn + container_definitions = var.container_definitions + cpu = var.cpu + create_iam_role = var.create_iam_role + create_infrastructure_iam_role = var.create_infrastructure_iam_role + create_security_group = var.create_security_group + create_service = var.create_service + create_task_definition = var.create_task_definition + create_task_exec_iam_role = var.create_task_exec_iam_role + create_task_exec_policy = var.create_task_exec_policy + create_tasks_iam_role = var.create_tasks_iam_role } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index 221859a5..c0f8a902 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -376,3 +376,51 @@ variable "cpu" { type = number default = 1024 } + +variable "create_iam_role" { + description = "Determines whether the ECS service IAM role should be created" + type = bool + default = true +} + +variable "create_infrastructure_iam_role" { + description = "Determines whether the ECS infrastructure IAM role should be created" + type = bool + default = true +} + +variable "create_security_group" { + description = "Determines if a security group is created" + type = bool + default = true +} + +variable "create_service" { + description = "Determines whether service resource will be created (set to `false` in case you want to create task definition only)" + type = bool + default = true +} + +variable "create_task_definition" { + description = "Determines whether to create a task definition or use existing/provided" + type = bool + default = true +} + +variable "create_task_exec_iam_role" { + description = "Determines whether the ECS task definition IAM role should be created" + type = bool + default = true +} + +variable "create_task_exec_policy" { + description = "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" + type = bool + default = true +} + +variable "create_tasks_iam_role" { + description = "Determines whether the ECS tasks IAM role should be created" + type = bool + default = true +} From 97c23b9ba5f978d3fbcd387f6437d327c392a65f Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 09:22:29 +0100 Subject: [PATCH 06/22] feat(ecs-service): add inputs d --- infrastructure/modules/ecs-service/README.md | 6 ++ infrastructure/modules/ecs-service/main.tf | 46 ++++++++------- .../modules/ecs-service/variables.tf | 58 +++++++++++++++++++ 3 files changed, 90 insertions(+), 20 deletions(-) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index 29e24624..c66e08f8 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -57,7 +57,13 @@ No resources. | [data\_classification](#input\_data\_classification) | Used to identify the data classification of the resource, e.g 1-5 | `string` | `"n/a"` | no | | [data\_type](#input\_data\_type) | The tag data\_type | `string` | `"None"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [deployment\_circuit\_breaker](#input\_deployment\_circuit\_breaker) | Configuration block for deployment circuit breaker |
object({
enable = bool
rollback = bool
})
| `null` | no | +| [deployment\_configuration](#input\_deployment\_configuration) | Configuration block for deployment settings |
object({
strategy = optional(string)
bake_time_in_minutes = optional(string)
canary_configuration = optional(object({
canary_bake_time_in_minutes = optional(string)
canary_percent = optional(string)
}))
linear_configuration = optional(object({
step_bake_time_in_minutes = optional(string)
step_percent = optional(string)
}))
lifecycle_hook = optional(map(object({
hook_target_arn = string
role_arn = optional(string)
lifecycle_stages = list(string)
hook_details = optional(string)
})))
})
| `null` | no | +| [deployment\_controller](#input\_deployment\_controller) | Configuration block for deployment controller configuration |
object({
type = optional(string)
})
| `null` | no | +| [deployment\_maximum\_percent](#input\_deployment\_maximum\_percent) | Upper limit (as a percentage of the service's `desired_count`) of the number of running tasks that can be running in a service during a deployment | `number` | `200` | no | +| [deployment\_minimum\_healthy\_percent](#input\_deployment\_minimum\_healthy\_percent) | Lower limit (as a percentage of the service's `desired_count`) of the number of running tasks that must remain running and healthy in a service during a deployment | `number` | `66` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [desired\_count](#input\_desired\_count) | Number of instances of the task definition to place and keep running | `number` | `1` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used to indicate role, e.g. 'prd', 'dev', 'test', 'preprod', 'prod', 'uat' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index 1ca8f043..921b7137 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -6,24 +6,30 @@ module "ecs_service" { name = module.this.name tags = module.this.tags - alarms = var.alarms - assign_public_ip = var.assign_public_ip - autoscaling_max_capacity = var.autoscaling_max_capacity - autoscaling_min_capacity = var.autoscaling_min_capacity - autoscaling_policies = var.autoscaling_policies - autoscaling_scheduled_actions = var.autoscaling_scheduled_actions - autoscaling_suspended_state = var.autoscaling_suspended_state - availability_zone_rebalancing = var.availability_zone_rebalancing - capacity_provider_strategy = var.capacity_provider_strategy - cluster_arn = var.cluster_arn - container_definitions = var.container_definitions - cpu = var.cpu - create_iam_role = var.create_iam_role - create_infrastructure_iam_role = var.create_infrastructure_iam_role - create_security_group = var.create_security_group - create_service = var.create_service - create_task_definition = var.create_task_definition - create_task_exec_iam_role = var.create_task_exec_iam_role - create_task_exec_policy = var.create_task_exec_policy - create_tasks_iam_role = var.create_tasks_iam_role + alarms = var.alarms + assign_public_ip = var.assign_public_ip + autoscaling_max_capacity = var.autoscaling_max_capacity + autoscaling_min_capacity = var.autoscaling_min_capacity + autoscaling_policies = var.autoscaling_policies + autoscaling_scheduled_actions = var.autoscaling_scheduled_actions + autoscaling_suspended_state = var.autoscaling_suspended_state + availability_zone_rebalancing = var.availability_zone_rebalancing + capacity_provider_strategy = var.capacity_provider_strategy + cluster_arn = var.cluster_arn + container_definitions = var.container_definitions + cpu = var.cpu + create_iam_role = var.create_iam_role + create_infrastructure_iam_role = var.create_infrastructure_iam_role + create_security_group = var.create_security_group + create_service = var.create_service + create_task_definition = var.create_task_definition + create_task_exec_iam_role = var.create_task_exec_iam_role + create_task_exec_policy = var.create_task_exec_policy + create_tasks_iam_role = var.create_tasks_iam_role + deployment_circuit_breaker = var.deployment_circuit_breaker + deployment_configuration = var.deployment_configuration + deployment_controller = var.deployment_controller + deployment_maximum_percent = var.deployment_maximum_percent + deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent + desired_count = var.desired_count } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index c0f8a902..82f48e7d 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -424,3 +424,61 @@ variable "create_tasks_iam_role" { type = bool default = true } + +variable "deployment_circuit_breaker" { + description = "Configuration block for deployment circuit breaker" + type = object({ + enable = bool + rollback = bool + }) + default = null +} + +variable "deployment_configuration" { + description = "Configuration block for deployment settings" + type = object({ + strategy = optional(string) + bake_time_in_minutes = optional(string) + canary_configuration = optional(object({ + canary_bake_time_in_minutes = optional(string) + canary_percent = optional(string) + })) + linear_configuration = optional(object({ + step_bake_time_in_minutes = optional(string) + step_percent = optional(string) + })) + lifecycle_hook = optional(map(object({ + hook_target_arn = string + role_arn = optional(string) + lifecycle_stages = list(string) + hook_details = optional(string) + }))) + }) + default = null +} + +variable "deployment_controller" { + description = "Configuration block for deployment controller configuration" + type = object({ + type = optional(string) + }) + default = null +} + +variable "deployment_maximum_percent" { + description = "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" + type = number + default = 200 +} + +variable "deployment_minimum_healthy_percent" { + description = "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" + type = number + default = 66 +} + +variable "desired_count" { + description = "Number of instances of the task definition to place and keep running" + type = number + default = 1 +} From 528506b81c929e82cd0676cfc6bee7a38b800d9b Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 10:10:05 +0100 Subject: [PATCH 07/22] feat(ecs-service): add inputs e --- infrastructure/modules/ecs-service/README.md | 6 +++ infrastructure/modules/ecs-service/main.tf | 6 +++ .../modules/ecs-service/variables.tf | 38 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index c66e08f8..9ed3b128 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -64,8 +64,14 @@ No resources. | [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 | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [desired\_count](#input\_desired\_count) | Number of instances of the task definition to place and keep running | `number` | `1` | no | +| [enable\_autoscaling](#input\_enable\_autoscaling) | Determines whether to enable autoscaling for the service | `bool` | `true` | no | +| [enable\_ecs\_managed\_tags](#input\_enable\_ecs\_managed\_tags) | Specifies whether to enable Amazon ECS managed tags for the tasks within the service | `bool` | `true` | no | +| [enable\_execute\_command](#input\_enable\_execute\_command) | Specifies whether to enable Amazon ECS Exec for the tasks within the service | `bool` | `false` | no | +| [enable\_fault\_injection](#input\_enable\_fault\_injection) | Enables fault injection and allows for fault injection requests to be accepted from the task's containers. Default is `false` | `bool` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used to indicate role, e.g. 'prd', 'dev', 'test', 'preprod', 'prod', 'uat' | `string` | `null` | no | +| [ephemeral\_storage](#input\_ephemeral\_storage) | The amount of ephemeral storage to allocate for the task. This parameter is used to expand the total amount of ephemeral storage available, beyond the default amount, for tasks hosted on AWS Fargate |
object({
size_in_gib = number
})
| `null` | no | +| [external\_id](#input\_external\_id) | The external ID associated with the task set | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index 921b7137..11d211b2 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -32,4 +32,10 @@ module "ecs_service" { deployment_maximum_percent = var.deployment_maximum_percent deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent desired_count = var.desired_count + enable_autoscaling = var.enable_autoscaling + enable_ecs_managed_tags = var.enable_ecs_managed_tags + enable_execute_command = var.enable_execute_command + enable_fault_injection = var.enable_fault_injection + ephemeral_storage = var.ephemeral_storage + external_id = var.external_id } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index 82f48e7d..0c96885a 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -482,3 +482,41 @@ variable "desired_count" { type = number default = 1 } + +variable "enable_autoscaling" { + description = "Determines whether to enable autoscaling for the service" + type = bool + default = true +} + +variable "enable_ecs_managed_tags" { + description = "Specifies whether to enable Amazon ECS managed tags for the tasks within the service" + type = bool + default = true +} + +variable "enable_execute_command" { + description = "Specifies whether to enable Amazon ECS Exec for the tasks within the service" + type = bool + default = false +} + +variable "enable_fault_injection" { + description = "Enables fault injection and allows for fault injection requests to be accepted from the task's containers. Default is `false`" + type = bool + default = null +} + +variable "ephemeral_storage" { + description = "The amount of ephemeral storage to allocate for the task. This parameter is used to expand the total amount of ephemeral storage available, beyond the default amount, for tasks hosted on AWS Fargate" + type = object({ + size_in_gib = number + }) + default = null +} + +variable "external_id" { + description = "The external ID associated with the task set" + type = string + default = null +} From dd3e1e28df418783530fee99668938915ff070bd Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 10:15:15 +0100 Subject: [PATCH 08/22] feat(ecs-service): add inputs f-h --- infrastructure/modules/ecs-service/README.md | 3 +++ infrastructure/modules/ecs-service/main.tf | 3 +++ .../modules/ecs-service/variables.tf | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index 9ed3b128..cd203a68 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -72,6 +72,9 @@ No resources. | [environment](#input\_environment) | ID element. Usually used to indicate role, e.g. 'prd', 'dev', 'test', 'preprod', 'prod', 'uat' | `string` | `null` | no | | [ephemeral\_storage](#input\_ephemeral\_storage) | The amount of ephemeral storage to allocate for the task. This parameter is used to expand the total amount of ephemeral storage available, beyond the default amount, for tasks hosted on AWS Fargate |
object({
size_in_gib = number
})
| `null` | no | | [external\_id](#input\_external\_id) | The external ID associated with the task set | `string` | `null` | no | +| [force\_delete](#input\_force\_delete) | Enable to delete a service even if it wasn't scaled down to zero tasks. It's only necessary to use this if the service uses the `REPLICA` scheduling strategy | `bool` | `null` | no | +| [force\_new\_deployment](#input\_force\_new\_deployment) | Enable to force a new task deployment of the service. This can be used to update tasks to use a newer Docker image with same image/tag combination, roll Fargate tasks onto a newer platform version, or immediately deploy `ordered_placement_strategy` and `placement_constraints` updates | `bool` | `true` | no | +| [health\_check\_grace\_period\_seconds](#input\_health\_check\_grace\_period\_seconds) | Seconds to ignore failing load balancer health checks on newly instantiated tasks to prevent premature shutdown, up to 2147483647. Only valid for services configured to use load balancers | `number` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index 11d211b2..13b535f2 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -38,4 +38,7 @@ module "ecs_service" { enable_fault_injection = var.enable_fault_injection ephemeral_storage = var.ephemeral_storage external_id = var.external_id + force_delete = var.force_delete + force_new_deployment = var.force_new_deployment + health_check_grace_period_seconds = var.health_check_grace_period_seconds } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index 0c96885a..1032fcfa 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -520,3 +520,21 @@ variable "external_id" { type = string default = null } + +variable "force_delete" { + description = "Enable to delete a service even if it wasn't scaled down to zero tasks. It's only necessary to use this if the service uses the `REPLICA` scheduling strategy" + type = bool + default = null +} + +variable "force_new_deployment" { + description = "Enable to force a new task deployment of the service. This can be used to update tasks to use a newer Docker image with same image/tag combination, roll Fargate tasks onto a newer platform version, or immediately deploy `ordered_placement_strategy` and `placement_constraints` updates" + type = bool + default = true +} + +variable "health_check_grace_period_seconds" { + description = "Seconds to ignore failing load balancer health checks on newly instantiated tasks to prevent premature shutdown, up to 2147483647. Only valid for services configured to use load balancers" + type = number + default = null +} From 6d11d4369e3ab42db13c82a2ec09a41b3e8195a5 Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 10:24:48 +0100 Subject: [PATCH 09/22] feat(ecs-service): add inputs ia --- infrastructure/modules/ecs-service/README.md | 8 +++ infrastructure/modules/ecs-service/main.tf | 8 +++ .../modules/ecs-service/variables.tf | 68 +++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index cd203a68..118f9d3d 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -75,6 +75,14 @@ No resources. | [force\_delete](#input\_force\_delete) | Enable to delete a service even if it wasn't scaled down to zero tasks. It's only necessary to use this if the service uses the `REPLICA` scheduling strategy | `bool` | `null` | no | | [force\_new\_deployment](#input\_force\_new\_deployment) | Enable to force a new task deployment of the service. This can be used to update tasks to use a newer Docker image with same image/tag combination, roll Fargate tasks onto a newer platform version, or immediately deploy `ordered_placement_strategy` and `placement_constraints` updates | `bool` | `true` | no | | [health\_check\_grace\_period\_seconds](#input\_health\_check\_grace\_period\_seconds) | Seconds to ignore failing load balancer health checks on newly instantiated tasks to prevent premature shutdown, up to 2147483647. Only valid for services configured to use load balancers | `number` | `null` | no | +| [iam\_role\_arn](#input\_iam\_role\_arn) | Existing IAM role ARN | `string` | `null` | no | +| [iam\_role\_description](#input\_iam\_role\_description) | Description of the role | `string` | `null` | no | +| [iam\_role\_name](#input\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | +| [iam\_role\_path](#input\_iam\_role\_path) | IAM role path | `string` | `null` | no | +| [iam\_role\_permissions\_boundary](#input\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [iam\_role\_statements](#input\_iam\_role\_statements) | A map of IAM policy statements for custom permission usage |
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
})))
}))
| `null` | no | +| [iam\_role\_tags](#input\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | +| [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 | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index 13b535f2..b3ad4100 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -41,4 +41,12 @@ module "ecs_service" { force_delete = var.force_delete force_new_deployment = var.force_new_deployment health_check_grace_period_seconds = var.health_check_grace_period_seconds + iam_role_arn = var.iam_role_arn + iam_role_description = var.iam_role_description + iam_role_name = var.iam_role_name + iam_role_path = var.iam_role_path + iam_role_permissions_boundary = var.iam_role_permissions_boundary + iam_role_statements = var.iam_role_statements + iam_role_tags = var.iam_role_tags + iam_role_use_name_prefix = var.iam_role_use_name_prefix } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index 1032fcfa..42c40a0f 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -538,3 +538,71 @@ variable "health_check_grace_period_seconds" { type = number default = null } + +variable "iam_role_arn" { + description = "Existing IAM role ARN" + type = string + default = null +} + +variable "iam_role_description" { + description = "Description of the role" + type = string + default = null +} + +variable "iam_role_name" { + description = "Name to use on IAM role created" + type = string + default = null +} + +variable "iam_role_path" { + description = "IAM role path" + type = string + default = null +} + +variable "iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "iam_role_statements" { + description = "A map of IAM policy statements for custom permission usage" + type = 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 + }))) + })) + default = null +} + +variable "iam_role_tags" { + description = "A map of additional tags to add to the IAM role created" + type = map(string) + default = {} +} + +variable "iam_role_use_name_prefix" { + description = "Determines whether the IAM role name (`iam_role_name`) is used as a prefix" + type = bool + default = true +} From e642161068c7429bb2bca92b06fe2b32907b7fa1 Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 10:31:24 +0100 Subject: [PATCH 10/22] feat(ecs-service): add inputs ig-ip --- infrastructure/modules/ecs-service/README.md | 9 ++ infrastructure/modules/ecs-service/main.tf | 95 ++++++++++--------- .../modules/ecs-service/variables.tf | 54 +++++++++++ 3 files changed, 115 insertions(+), 43 deletions(-) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index 118f9d3d..cc60883b 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -84,6 +84,15 @@ No resources. | [iam\_role\_tags](#input\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | | [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 | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [ignore\_task\_definition\_changes](#input\_ignore\_task\_definition\_changes) | Whether changes to service `task_definition` changes should be ignored | `bool` | `false` | no | +| [infrastructure\_iam\_role\_arn](#input\_infrastructure\_iam\_role\_arn) | Existing IAM role ARN | `string` | `null` | no | +| [infrastructure\_iam\_role\_description](#input\_infrastructure\_iam\_role\_description) | Description of the role | `string` | `null` | no | +| [infrastructure\_iam\_role\_name](#input\_infrastructure\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | +| [infrastructure\_iam\_role\_path](#input\_infrastructure\_iam\_role\_path) | IAM role path | `string` | `null` | no | +| [infrastructure\_iam\_role\_permissions\_boundary](#input\_infrastructure\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [infrastructure\_iam\_role\_tags](#input\_infrastructure\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | +| [infrastructure\_iam\_role\_use\_name\_prefix](#input\_infrastructure\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`iam_role_name`) is used as a prefix | `bool` | `true` | no | +| [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 | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index b3ad4100..ffeb7d57 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -6,47 +6,56 @@ module "ecs_service" { name = module.this.name tags = module.this.tags - alarms = var.alarms - assign_public_ip = var.assign_public_ip - autoscaling_max_capacity = var.autoscaling_max_capacity - autoscaling_min_capacity = var.autoscaling_min_capacity - autoscaling_policies = var.autoscaling_policies - autoscaling_scheduled_actions = var.autoscaling_scheduled_actions - autoscaling_suspended_state = var.autoscaling_suspended_state - availability_zone_rebalancing = var.availability_zone_rebalancing - capacity_provider_strategy = var.capacity_provider_strategy - cluster_arn = var.cluster_arn - container_definitions = var.container_definitions - cpu = var.cpu - create_iam_role = var.create_iam_role - create_infrastructure_iam_role = var.create_infrastructure_iam_role - create_security_group = var.create_security_group - create_service = var.create_service - create_task_definition = var.create_task_definition - create_task_exec_iam_role = var.create_task_exec_iam_role - create_task_exec_policy = var.create_task_exec_policy - create_tasks_iam_role = var.create_tasks_iam_role - deployment_circuit_breaker = var.deployment_circuit_breaker - deployment_configuration = var.deployment_configuration - deployment_controller = var.deployment_controller - deployment_maximum_percent = var.deployment_maximum_percent - deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent - desired_count = var.desired_count - enable_autoscaling = var.enable_autoscaling - enable_ecs_managed_tags = var.enable_ecs_managed_tags - enable_execute_command = var.enable_execute_command - enable_fault_injection = var.enable_fault_injection - ephemeral_storage = var.ephemeral_storage - external_id = var.external_id - force_delete = var.force_delete - force_new_deployment = var.force_new_deployment - health_check_grace_period_seconds = var.health_check_grace_period_seconds - iam_role_arn = var.iam_role_arn - iam_role_description = var.iam_role_description - iam_role_name = var.iam_role_name - iam_role_path = var.iam_role_path - iam_role_permissions_boundary = var.iam_role_permissions_boundary - iam_role_statements = var.iam_role_statements - iam_role_tags = var.iam_role_tags - iam_role_use_name_prefix = var.iam_role_use_name_prefix + alarms = var.alarms + assign_public_ip = var.assign_public_ip + autoscaling_max_capacity = var.autoscaling_max_capacity + autoscaling_min_capacity = var.autoscaling_min_capacity + autoscaling_policies = var.autoscaling_policies + autoscaling_scheduled_actions = var.autoscaling_scheduled_actions + autoscaling_suspended_state = var.autoscaling_suspended_state + availability_zone_rebalancing = var.availability_zone_rebalancing + capacity_provider_strategy = var.capacity_provider_strategy + cluster_arn = var.cluster_arn + container_definitions = var.container_definitions + cpu = var.cpu + create_iam_role = var.create_iam_role + create_infrastructure_iam_role = var.create_infrastructure_iam_role + create_security_group = var.create_security_group + create_service = var.create_service + create_task_definition = var.create_task_definition + create_task_exec_iam_role = var.create_task_exec_iam_role + create_task_exec_policy = var.create_task_exec_policy + create_tasks_iam_role = var.create_tasks_iam_role + deployment_circuit_breaker = var.deployment_circuit_breaker + deployment_configuration = var.deployment_configuration + deployment_controller = var.deployment_controller + deployment_maximum_percent = var.deployment_maximum_percent + deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent + desired_count = var.desired_count + enable_autoscaling = var.enable_autoscaling + enable_ecs_managed_tags = var.enable_ecs_managed_tags + enable_execute_command = var.enable_execute_command + enable_fault_injection = var.enable_fault_injection + ephemeral_storage = var.ephemeral_storage + external_id = var.external_id + force_delete = var.force_delete + force_new_deployment = var.force_new_deployment + health_check_grace_period_seconds = var.health_check_grace_period_seconds + iam_role_arn = var.iam_role_arn + iam_role_description = var.iam_role_description + iam_role_name = var.iam_role_name + iam_role_path = var.iam_role_path + iam_role_permissions_boundary = var.iam_role_permissions_boundary + iam_role_statements = var.iam_role_statements + iam_role_tags = var.iam_role_tags + iam_role_use_name_prefix = var.iam_role_use_name_prefix + ignore_task_definition_changes = var.ignore_task_definition_changes + infrastructure_iam_role_arn = var.infrastructure_iam_role_arn + infrastructure_iam_role_description = var.infrastructure_iam_role_description + infrastructure_iam_role_name = var.infrastructure_iam_role_name + infrastructure_iam_role_path = var.infrastructure_iam_role_path + infrastructure_iam_role_permissions_boundary = var.infrastructure_iam_role_permissions_boundary + infrastructure_iam_role_tags = var.infrastructure_iam_role_tags + infrastructure_iam_role_use_name_prefix = var.infrastructure_iam_role_use_name_prefix + ipc_mode = var.ipc_mode } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index 42c40a0f..bd633040 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -606,3 +606,57 @@ variable "iam_role_use_name_prefix" { type = bool default = true } + +variable "ignore_task_definition_changes" { + description = "Whether changes to service `task_definition` changes should be ignored" + type = bool + default = false +} + +variable "infrastructure_iam_role_arn" { + description = "Existing IAM role ARN" + type = string + default = null +} + +variable "infrastructure_iam_role_description" { + description = "Description of the role" + type = string + default = null +} + +variable "infrastructure_iam_role_name" { + description = "Name to use on IAM role created" + type = string + default = null +} + +variable "infrastructure_iam_role_path" { + description = "IAM role path" + type = string + default = null +} + +variable "infrastructure_iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "infrastructure_iam_role_tags" { + description = "A map of additional tags to add to the IAM role created" + type = map(string) + default = {} +} + +variable "infrastructure_iam_role_use_name_prefix" { + description = "Determines whether the IAM role name (`iam_role_name`) is used as a prefix" + type = bool + default = true +} + +variable "ipc_mode" { + description = "IPC resource namespace to be used for the containers in the task The valid values are `host`, `task`, and `none`" + type = string + default = null +} From 024c0c2f94d5a117170b819af3bcd5e41c2e13ad Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 10:51:38 +0100 Subject: [PATCH 11/22] feat(ecs-service): add inputs l-p --- infrastructure/modules/ecs-service/README.md | 10 +++ infrastructure/modules/ecs-service/main.tf | 10 +++ .../modules/ecs-service/variables.tf | 81 +++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index cc60883b..fe921665 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -97,10 +97,20 @@ No resources. | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [launch\_type](#input\_launch\_type) | Launch type on which to run your service. The valid values are `EC2`, `FARGATE`, and `EXTERNAL`. Defaults to `FARGATE` | `string` | `"FARGATE"` | no | +| [load\_balancer](#input\_load\_balancer) | Configuration block for load balancers |
map(object({
container_name = string
container_port = number
elb_name = optional(string)
target_group_arn = optional(string)
advanced_configuration = optional(object({
alternate_target_group_arn = string
production_listener_rule = string # Should be optional but bug in provider
role_arn = optional(string)
test_listener_rule = optional(string)
}))
}))
| `null` | no | +| [memory](#input\_memory) | Amount of memory (in MiB) used by the task. If the `requires_compatibilities` is `FARGATE` this field is required | `number` | `2048` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `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 | | [on\_off\_pattern](#input\_on\_off\_pattern) | Used to turn resources on and off based on a time pattern | `string` | `"n/a"` | no | +| [ordered\_placement\_strategy](#input\_ordered\_placement\_strategy) | Service level strategy rules that are taken into consideration during task placement. List from top to bottom in order of precedence |
list(object({
field = optional(string)
type = string
}))
| `null` | no | | [owner](#input\_owner) | The name and or NHS.net email address of the service owner | `string` | `"None"` | no | +| [pid\_mode](#input\_pid\_mode) | Process namespace to use for the containers in the task. The valid values are `host` and `task` | `string` | `null` | no | +| [placement\_constraints](#input\_placement\_constraints) | Configuration block for rules that are taken into consideration during task placement (up to max of 10). This is set at the service, see `task_definition_placement_constraints` for setting at the task definition |
map(object({
expression = optional(string)
type = string
}))
| `null` | no | +| [platform\_version](#input\_platform\_version) | Platform version on which to run your service. Only applicable for `launch_type` set to `FARGATE`. Defaults to `LATEST` | `string` | `null` | no | | [project](#input\_project) | ID element. A project identifier, indicating the name or role of the project the resource is for, such as `website` or `api` | `string` | `null` | no | +| [propagate\_tags](#input\_propagate\_tags) | Specifies whether to propagate the tags from the task definition or the service to the tasks. The valid values are `SERVICE` and `TASK_DEFINITION` | `string` | `null` | no | +| [proxy\_configuration](#input\_proxy\_configuration) | Configuration block for the App Mesh proxy |
object({
container_name = string
properties = optional(map(string))
type = optional(string)
})
| `null` | no | | [public\_facing](#input\_public\_facing) | Whether this resource is public facing | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | ID element \_(Rarely used, not included by default)\_. Usually an abbreviation of the selected AWS region e.g. 'uw2', 'ew2' or 'gbl' for resources like IAM roles that have no region | `string` | `null` | no | diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index ffeb7d57..ca8c8a3f 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -58,4 +58,14 @@ module "ecs_service" { infrastructure_iam_role_tags = var.infrastructure_iam_role_tags infrastructure_iam_role_use_name_prefix = var.infrastructure_iam_role_use_name_prefix ipc_mode = var.ipc_mode + launch_type = var.launch_type + load_balancer = var.load_balancer + memory = var.memory + network_mode = var.network_mode + ordered_placement_strategy = var.ordered_placement_strategy + pid_mode = var.pid_mode + placement_constraints = var.placement_constraints + platform_version = var.platform_version + propagate_tags = var.propagate_tags + proxy_configuration = var.proxy_configuration } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index bd633040..adaab9f7 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -660,3 +660,84 @@ variable "ipc_mode" { type = string default = null } + +variable "launch_type" { + description = "Launch type on which to run your service. The valid values are `EC2`, `FARGATE`, and `EXTERNAL`. Defaults to `FARGATE`" + type = string + default = "FARGATE" +} + +variable "load_balancer" { + description = "Configuration block for load balancers" + type = map(object({ + container_name = string + container_port = number + elb_name = optional(string) + target_group_arn = optional(string) + advanced_configuration = optional(object({ + alternate_target_group_arn = string + production_listener_rule = string # Should be optional but bug in provider + role_arn = optional(string) + test_listener_rule = optional(string) + })) + })) + default = null +} + +variable "memory" { + description = "Amount of memory (in MiB) used by the task. If the `requires_compatibilities` is `FARGATE` this field is required" + type = number + default = 2048 +} + +variable "network_mode" { + description = "Docker networking mode to use for the containers in the task. Valid values are `none`, `bridge`, `awsvpc`, and `host`" + type = string + default = "awsvpc" +} + +variable "ordered_placement_strategy" { + description = "Service level strategy rules that are taken into consideration during task placement. List from top to bottom in order of precedence" + type = list(object({ + field = optional(string) + type = string + })) + default = null +} + +variable "pid_mode" { + description = "Process namespace to use for the containers in the task. The valid values are `host` and `task`" + type = string + default = null +} + +variable "placement_constraints" { + description = "Configuration block for rules that are taken into consideration during task placement (up to max of 10). This is set at the service, see `task_definition_placement_constraints` for setting at the task definition" + type = map(object({ + expression = optional(string) + type = string + })) + default = null +} + +variable "platform_version" { + description = "Platform version on which to run your service. Only applicable for `launch_type` set to `FARGATE`. Defaults to `LATEST`" + type = string + default = null +} + +variable "propagate_tags" { + description = "Specifies whether to propagate the tags from the task definition or the service to the tasks. The valid values are `SERVICE` and `TASK_DEFINITION`" + type = string + default = null +} + +variable "proxy_configuration" { + description = "Configuration block for the App Mesh proxy" + type = object({ + container_name = string + properties = optional(map(string)) + type = optional(string) + }) + default = null +} From f6e524cca1c70270552056ca93d19ac63e3d4286 Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 10:58:15 +0100 Subject: [PATCH 12/22] feat(ecs-service): add inputs r-sc --- infrastructure/modules/ecs-service/README.md | 4 +++ infrastructure/modules/ecs-service/main.tf | 4 +++ .../modules/ecs-service/variables.tf | 30 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index fe921665..167e2dde 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -114,6 +114,10 @@ No resources. | [public\_facing](#input\_public\_facing) | Whether this resource is public facing | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | ID element \_(Rarely used, not included by default)\_. Usually an abbreviation of the selected AWS region e.g. 'uw2', 'ew2' or 'gbl' for resources like IAM roles that have no region | `string` | `null` | no | +| [requires\_compatibilities](#input\_requires\_compatibilities) | Set of launch types required by the task. The valid values are `EC2`, `FARGATE`, `EXTERNAL`, and `MANAGED_INSTANCES` | `list(string)` |
[
"FARGATE"
]
| no | +| [runtime\_platform](#input\_runtime\_platform) | Configuration block for `runtime_platform` that containers in your task may use |
object({
cpu_architecture = optional(string, "X86_64")
operating_system_family = optional(string, "LINUX")
})
| `{}` | no | +| [scale](#input\_scale) | A floating-point percentage of the desired number of tasks to place and keep running in the task set |
object({
unit = optional(string)
value = optional(number)
})
| `null` | no | +| [scheduling\_strategy](#input\_scheduling\_strategy) | Scheduling strategy to use for the service. The valid values are `REPLICA` and `DAEMON`. Defaults to `REPLICA` | `string` | `null` | no | | [service](#input\_service) | ID element. Usually an abbreviation of your service directorate name, e.g. 'bcss' or 'csms', to help ensure generated IDs are globally unique | `string` | `null` | no | | [service\_category](#input\_service\_category) | The tag service\_category | `string` | `"n/a"` | no | | [stack](#input\_stack) | ID element. The name of the stack/component, e.g. `database`, `web`, `waf`, `eks` | `string` | `null` | no | diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index ca8c8a3f..5d51a407 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -68,4 +68,8 @@ module "ecs_service" { platform_version = var.platform_version propagate_tags = var.propagate_tags proxy_configuration = var.proxy_configuration + requires_compatibilities = var.requires_compatibilities + runtime_platform = var.runtime_platform + scale = var.scale + scheduling_strategy = var.scheduling_strategy } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index adaab9f7..9d694057 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -741,3 +741,33 @@ variable "proxy_configuration" { }) default = null } + +variable "requires_compatibilities" { + description = "Set of launch types required by the task. The valid values are `EC2`, `FARGATE`, `EXTERNAL`, and `MANAGED_INSTANCES`" + type = list(string) + default = ["FARGATE"] +} + +variable "runtime_platform" { + description = "Configuration block for `runtime_platform` that containers in your task may use" + type = object({ + cpu_architecture = optional(string, "X86_64") + operating_system_family = optional(string, "LINUX") + }) + default = {} +} + +variable "scale" { + description = "A floating-point percentage of the desired number of tasks to place and keep running in the task set" + type = object({ + unit = optional(string) + value = optional(number) + }) + default = null +} + +variable "scheduling_strategy" { + description = "Scheduling strategy to use for the service. The valid values are `REPLICA` and `DAEMON`. Defaults to `REPLICA`" + type = string + default = null +} From c643a1677d60b023c5b2d31ba7e36ca39ea2f0e0 Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 11:56:42 +0100 Subject: [PATCH 13/22] feat(ecs-service): add inputs s --- infrastructure/modules/ecs-service/README.md | 13 ++ infrastructure/modules/ecs-service/main.tf | 13 ++ .../modules/ecs-service/variables.tf | 148 ++++++++++++++++++ 3 files changed, 174 insertions(+) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index 167e2dde..1b3b70ad 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -118,9 +118,22 @@ No resources. | [runtime\_platform](#input\_runtime\_platform) | Configuration block for `runtime_platform` that containers in your task may use |
object({
cpu_architecture = optional(string, "X86_64")
operating_system_family = optional(string, "LINUX")
})
| `{}` | no | | [scale](#input\_scale) | A floating-point percentage of the desired number of tasks to place and keep running in the task set |
object({
unit = optional(string)
value = optional(number)
})
| `null` | no | | [scheduling\_strategy](#input\_scheduling\_strategy) | Scheduling strategy to use for the service. The valid values are `REPLICA` and `DAEMON`. Defaults to `REPLICA` | `string` | `null` | no | +| [security\_group\_description](#input\_security\_group\_description) | Description of the security group created | `string` | `null` | no | +| [security\_group\_egress\_rules](#input\_security\_group\_egress\_rules) | Security group egress rules to add to the security group created |
map(object({
name = optional(string)
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
}))
| `{}` | no | +| [security\_group\_ids](#input\_security\_group\_ids) | List of security groups to associate with the task or service | `list(string)` | `[]` | no | +| [security\_group\_ingress\_rules](#input\_security\_group\_ingress\_rules) | Security group ingress rules to add to the security group created |
map(object({
name = optional(string)
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(string)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(string)
}))
| `{}` | no | +| [security\_group\_name](#input\_security\_group\_name) | Name to use on security group created | `string` | `null` | no | +| [security\_group\_tags](#input\_security\_group\_tags) | A map of additional tags to add to the security group created | `map(string)` | `{}` | no | +| [security\_group\_use\_name\_prefix](#input\_security\_group\_use\_name\_prefix) | Determines whether the security group name (`security_group_name`) is used as a prefix | `bool` | `true` | no | | [service](#input\_service) | ID element. Usually an abbreviation of your service directorate name, e.g. 'bcss' or 'csms', to help ensure generated IDs are globally unique | `string` | `null` | no | | [service\_category](#input\_service\_category) | The tag service\_category | `string` | `"n/a"` | 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)
access_log_configuration = optional(object({
format = string
include_query_parameters = optional(string)
}))
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 | +| [sigint\_rollback](#input\_sigint\_rollback) | Whether to enable graceful termination of deployments using SIGINT signals. Only applicable when using ECS deployment controller and requires wait\_for\_steady\_state = true. Default is false | `bool` | `null` | no | +| [skip\_destroy](#input\_skip\_destroy) | If true, the task is not deleted when the service is deleted | `bool` | `null` | no | | [stack](#input\_stack) | ID element. The name of the stack/component, e.g. `database`, `web`, `waf`, `eks` | `string` | `null` | no | +| [subnet\_ids](#input\_subnet\_ids) | List of subnets to associate with the task or service | `list(string)` | `[]` | no | | [tag\_version](#input\_tag\_version) | Used to identify the tagging version in use | `string` | `"1.0"` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [terraform\_source](#input\_terraform\_source) | Source location to record in the Terraform\_source tag. Defaults to this module path. | `string` | `null` | no | diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index 5d51a407..6d69af18 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -72,4 +72,17 @@ module "ecs_service" { runtime_platform = var.runtime_platform scale = var.scale scheduling_strategy = var.scheduling_strategy + security_group_description = var.security_group_description + security_group_egress_rules = var.security_group_egress_rules + security_group_ids = var.security_group_ids + security_group_ingress_rules = var.security_group_ingress_rules + security_group_name = var.security_group_name + security_group_tags = var.security_group_tags + security_group_use_name_prefix = var.security_group_use_name_prefix + service_connect_configuration = var.service_connect_configuration + service_registries = var.service_registries + service_tags = var.service_tags + sigint_rollback = var.sigint_rollback + skip_destroy = var.skip_destroy + subnet_ids = var.subnet_ids } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index 9d694057..f048b8f5 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -771,3 +771,151 @@ variable "scheduling_strategy" { type = string default = null } + +variable "security_group_description" { + description = "Description of the security group created" + type = string + default = null +} + +variable "security_group_egress_rules" { + description = "Security group egress rules to add to the security group created" + type = map(object({ + name = optional(string) + cidr_ipv4 = optional(string) + cidr_ipv6 = optional(string) + description = optional(string) + from_port = optional(string) + ip_protocol = optional(string, "tcp") + prefix_list_id = optional(string) + referenced_security_group_id = optional(string) + tags = optional(map(string), {}) + to_port = optional(string) + })) + default = {} +} + +variable "security_group_ids" { + description = "List of security groups to associate with the task or service" + type = list(string) + default = [] +} + +variable "security_group_ingress_rules" { + description = "Security group ingress rules to add to the security group created" + type = map(object({ + name = optional(string) + cidr_ipv4 = optional(string) + cidr_ipv6 = optional(string) + description = optional(string) + from_port = optional(string) + ip_protocol = optional(string, "tcp") + prefix_list_id = optional(string) + referenced_security_group_id = optional(string) + tags = optional(map(string), {}) + to_port = optional(string) + })) + default = {} +} + +variable "security_group_name" { + description = "Name to use on security group created" + type = string + default = null +} + +variable "security_group_tags" { + description = "A map of additional tags to add to the security group created" + type = map(string) + default = {} +} + +variable "security_group_use_name_prefix" { + description = "Determines whether the security group name (`security_group_name`) is used as a prefix" + type = bool + default = true +} + +variable "service_connect_configuration" { + description = "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" + type = object({ + enabled = optional(bool, true) + access_log_configuration = optional(object({ + format = string + include_query_parameters = optional(string) + })) + 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) + })) + }))) + }) + default = null +} + +variable "service_registries" { + description = "Service discovery registries for the service" + type = object({ + container_name = optional(string) + container_port = optional(number) + port = optional(number) + registry_arn = string + }) + default = null +} + +variable "service_tags" { + description = "A map of additional tags to add to the service" + type = map(string) + default = {} +} + +variable "sigint_rollback" { + description = "Whether to enable graceful termination of deployments using SIGINT signals. Only applicable when using ECS deployment controller and requires wait_for_steady_state = true. Default is false" + type = bool + default = null +} + +variable "skip_destroy" { + description = "If true, the task is not deleted when the service is deleted" + type = bool + default = null +} + +variable "subnet_ids" { + description = "List of subnets to associate with the task or service" + type = list(string) + default = [] +} From a81de960ad3969aaf32427b6099e8d6d286136c5 Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 12:11:27 +0100 Subject: [PATCH 14/22] feat(ecs-service): add inputs task_* --- infrastructure/modules/ecs-service/README.md | 16 +++ infrastructure/modules/ecs-service/main.tf | 17 +++ .../modules/ecs-service/variables.tf | 119 ++++++++++++++++++ 3 files changed, 152 insertions(+) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index 1b3b70ad..5602a126 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -136,6 +136,22 @@ No resources. | [subnet\_ids](#input\_subnet\_ids) | List of subnets to associate with the task or service | `list(string)` | `[]` | no | | [tag\_version](#input\_tag\_version) | Used to identify the tagging version in use | `string` | `"1.0"` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [task\_definition\_arn](#input\_task\_definition\_arn) | Existing task definition ARN. Required when `create_task_definition` is `false` | `string` | `null` | no | +| [task\_definition\_placement\_constraints](#input\_task\_definition\_placement\_constraints) | Configuration block for rules that are taken into consideration during task placement (up to max of 10). This is set at the task definition, see `placement_constraints` for setting at the service |
map(object({
expression = optional(string)
type = string
}))
| `null` | no | +| [task\_exec\_iam\_policy\_path](#input\_task\_exec\_iam\_policy\_path) | Path for the iam role | `string` | `null` | no | +| [task\_exec\_iam\_role\_arn](#input\_task\_exec\_iam\_role\_arn) | Existing IAM role ARN | `string` | `null` | no | +| [task\_exec\_iam\_role\_description](#input\_task\_exec\_iam\_role\_description) | Description of the role | `string` | `null` | no | +| [task\_exec\_iam\_role\_max\_session\_duration](#input\_task\_exec\_iam\_role\_max\_session\_duration) | Maximum session duration (in seconds) for ECS task execution role. Default is 3600. | `number` | `null` | no | +| [task\_exec\_iam\_role\_name](#input\_task\_exec\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | +| [task\_exec\_iam\_role\_path](#input\_task\_exec\_iam\_role\_path) | IAM role path | `string` | `null` | no | +| [task\_exec\_iam\_role\_permissions\_boundary](#input\_task\_exec\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [task\_exec\_iam\_role\_policies](#input\_task\_exec\_iam\_role\_policies) | Map of IAM role policy ARNs to attach to the IAM role | `map(string)` | `null` | no | +| [task\_exec\_iam\_role\_tags](#input\_task\_exec\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | +| [task\_exec\_iam\_role\_use\_name\_prefix](#input\_task\_exec\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`task_exec_iam_role_name`) is used as a prefix | `bool` | `true` | no | +| [task\_exec\_iam\_statements](#input\_task\_exec\_iam\_statements) | A map of IAM policy statements for custom permission usage |
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
})))
}))
| `null` | no | +| [task\_exec\_secret\_arns](#input\_task\_exec\_secret\_arns) | List of SecretsManager secret ARNs the task execution role will be permitted to get/read | `list(string)` | `[]` | no | +| [task\_exec\_ssm\_param\_arns](#input\_task\_exec\_ssm\_param\_arns) | List of SSM parameter ARNs the task execution role will be permitted to get/read | `list(string)` | `[]` | no | +| [task\_tags](#input\_task\_tags) | A map of additional tags to add to the task definition/set created | `map(string)` | `{}` | no | | [terraform\_source](#input\_terraform\_source) | Source location to record in the Terraform\_source tag. Defaults to this module path. | `string` | `null` | no | | [tool](#input\_tool) | The tool used to deploy the resource | `string` | `"Terraform"` | no | | [workspace](#input\_workspace) | ID element. The Terraform workspace, to help ensure generated IDs are unique across workspaces | `string` | `null` | no | diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index 6d69af18..9cfb37fd 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -85,4 +85,21 @@ module "ecs_service" { sigint_rollback = var.sigint_rollback skip_destroy = var.skip_destroy subnet_ids = var.subnet_ids + task_definition_arn = var.task_definition_arn + task_definition_placement_constraints = var.task_definition_placement_constraints + task_exec_iam_policy_path = var.task_exec_iam_policy_path + task_exec_iam_role_arn = var.task_exec_iam_role_arn + task_exec_iam_role_description = var.task_exec_iam_role_description + task_exec_iam_role_max_session_duration = var.task_exec_iam_role_max_session_duration + task_exec_iam_role_name = var.task_exec_iam_role_name + task_exec_iam_role_path = var.task_exec_iam_role_path + task_exec_iam_role_permissions_boundary = var.task_exec_iam_role_permissions_boundary + task_exec_iam_role_policies = var.task_exec_iam_role_policies + task_exec_iam_role_tags = var.task_exec_iam_role_tags + task_exec_iam_role_use_name_prefix = var.task_exec_iam_role_use_name_prefix + task_exec_iam_statements = var.task_exec_iam_statements + task_exec_secret_arns = var.task_exec_secret_arns + task_exec_ssm_param_arns = var.task_exec_ssm_param_arns + task_tags = var.task_tags + } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index f048b8f5..b85608fc 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -919,3 +919,122 @@ variable "subnet_ids" { type = list(string) default = [] } + +variable "task_definition_arn" { + description = "Existing task definition ARN. Required when `create_task_definition` is `false`" + type = string + default = null +} + +variable "task_definition_placement_constraints" { + description = "Configuration block for rules that are taken into consideration during task placement (up to max of 10). This is set at the task definition, see `placement_constraints` for setting at the service" + type = map(object({ + expression = optional(string) + type = string + })) + default = null +} + +variable "task_exec_iam_policy_path" { + description = "Path for the iam role" + type = string + default = null +} + +variable "task_exec_iam_role_arn" { + description = "Existing IAM role ARN" + type = string + default = null +} + +variable "task_exec_iam_role_description" { + description = "Description of the role" + type = string + default = null +} + +variable "task_exec_iam_role_max_session_duration" { + description = "Maximum session duration (in seconds) for ECS task execution role. Default is 3600." + type = number + default = null +} + +variable "task_exec_iam_role_name" { + description = "Name to use on IAM role created" + type = string + default = null +} + +variable "task_exec_iam_role_path" { + description = "IAM role path" + type = string + default = null +} + +variable "task_exec_iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "task_exec_iam_role_policies" { + description = "Map of IAM role policy ARNs to attach to the IAM role" + type = map(string) + default = null +} + +variable "task_exec_iam_role_tags" { + description = "A map of additional tags to add to the IAM role created" + type = map(string) + default = {} +} + +variable "task_exec_iam_role_use_name_prefix" { + description = "Determines whether the IAM role name (`task_exec_iam_role_name`) is used as a prefix" + type = bool + default = true +} + +variable "task_exec_iam_statements" { + description = "A map of IAM policy statements for custom permission usage" + type = 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 + }))) + })) + default = null +} + +variable "task_exec_secret_arns" { + description = "List of SecretsManager secret ARNs the task execution role will be permitted to get/read" + type = list(string) + default = [] +} + +variable "task_exec_ssm_param_arns" { + description = "List of SSM parameter ARNs the task execution role will be permitted to get/read" + type = list(string) + default = [] +} + +variable "task_tags" { + description = "A map of additional tags to add to the task definition/set created" + type = map(string) + default = {} +} From d531b247dcd3c37923085893ed97e76102580999 Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 12:26:51 +0100 Subject: [PATCH 15/22] feat(ecs-service): add inputs tasks_* --- infrastructure/modules/ecs-service/README.md | 10 +++ infrastructure/modules/ecs-service/main.tf | 11 ++- .../modules/ecs-service/variables.tf | 80 +++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index 5602a126..3c7f87f9 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -152,6 +152,16 @@ No resources. | [task\_exec\_secret\_arns](#input\_task\_exec\_secret\_arns) | List of SecretsManager secret ARNs the task execution role will be permitted to get/read | `list(string)` | `[]` | no | | [task\_exec\_ssm\_param\_arns](#input\_task\_exec\_ssm\_param\_arns) | List of SSM parameter ARNs the task execution role will be permitted to get/read | `list(string)` | `[]` | no | | [task\_tags](#input\_task\_tags) | A map of additional tags to add to the task definition/set created | `map(string)` | `{}` | no | +| [tasks\_iam\_role\_arn](#input\_tasks\_iam\_role\_arn) | Existing IAM role ARN | `string` | `null` | no | +| [tasks\_iam\_role\_description](#input\_tasks\_iam\_role\_description) | Description of the role | `string` | `null` | no | +| [tasks\_iam\_role\_max\_session\_duration](#input\_tasks\_iam\_role\_max\_session\_duration) | Maximum session duration (in seconds) for ECS tasks role. Default is 3600. | `number` | `null` | no | +| [tasks\_iam\_role\_name](#input\_tasks\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | +| [tasks\_iam\_role\_path](#input\_tasks\_iam\_role\_path) | IAM role path | `string` | `null` | no | +| [tasks\_iam\_role\_permissions\_boundary](#input\_tasks\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [tasks\_iam\_role\_policies](#input\_tasks\_iam\_role\_policies) | Map of additional IAM role policy ARNs to attach to the IAM role | `map(string)` | `null` | no | +| [tasks\_iam\_role\_statements](#input\_tasks\_iam\_role\_statements) | A map of IAM policy statements for custom permission usage |
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
})))
}))
| `null` | no | +| [tasks\_iam\_role\_tags](#input\_tasks\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | +| [tasks\_iam\_role\_use\_name\_prefix](#input\_tasks\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`tasks_iam_role_name`) is used as a prefix | `bool` | `true` | no | | [terraform\_source](#input\_terraform\_source) | Source location to record in the Terraform\_source tag. Defaults to this module path. | `string` | `null` | no | | [tool](#input\_tool) | The tool used to deploy the resource | `string` | `"Terraform"` | no | | [workspace](#input\_workspace) | ID element. The Terraform workspace, to help ensure generated IDs are unique across workspaces | `string` | `null` | no | diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index 9cfb37fd..0fd21b71 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -101,5 +101,14 @@ module "ecs_service" { task_exec_secret_arns = var.task_exec_secret_arns task_exec_ssm_param_arns = var.task_exec_ssm_param_arns task_tags = var.task_tags - + tasks_iam_role_arn = var.tasks_iam_role_arn + tasks_iam_role_description = var.tasks_iam_role_description + tasks_iam_role_max_session_duration = var.tasks_iam_role_max_session_duration + tasks_iam_role_name = var.tasks_iam_role_name + tasks_iam_role_path = var.tasks_iam_role_path + tasks_iam_role_permissions_boundary = var.tasks_iam_role_permissions_boundary + tasks_iam_role_policies = var.tasks_iam_role_policies + tasks_iam_role_statements = var.tasks_iam_role_statements + tasks_iam_role_tags = var.tasks_iam_role_tags + tasks_iam_role_use_name_prefix = var.tasks_iam_role_use_name_prefix } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index b85608fc..2007b9aa 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -1038,3 +1038,83 @@ variable "task_tags" { type = map(string) default = {} } + +variable "tasks_iam_role_arn" { + description = "Existing IAM role ARN" + type = string + default = null +} + +variable "tasks_iam_role_description" { + description = "Description of the role" + type = string + default = null +} + +variable "tasks_iam_role_max_session_duration" { + description = "Maximum session duration (in seconds) for ECS tasks role. Default is 3600." + type = number + default = null +} + +variable "tasks_iam_role_name" { + description = "Name to use on IAM role created" + type = string + default = null +} + +variable "tasks_iam_role_path" { + description = "IAM role path" + type = string + default = null +} + +variable "tasks_iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "tasks_iam_role_policies" { + description = "Map of additional IAM role policy ARNs to attach to the IAM role" + type = map(string) + default = null +} + +variable "tasks_iam_role_statements" { + description = "A map of IAM policy statements for custom permission usage" + type = 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 + }))) + })) + default = null +} + +variable "tasks_iam_role_tags" { + description = "A map of additional tags to add to the IAM role created" + type = map(string) + default = {} +} + +variable "tasks_iam_role_use_name_prefix" { + description = "Determines whether the IAM role name (`tasks_iam_role_name`) is used as a prefix" + type = bool + default = true +} From a60ab2a7c1e876e31513c9582f58ea7eba234826 Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 12:34:04 +0100 Subject: [PATCH 16/22] feat(ecs-service): add inputs ti-z --- infrastructure/modules/ecs-service/README.md | 10 ++ infrastructure/modules/ecs-service/main.tf | 10 ++ .../modules/ecs-service/variables.tf | 115 ++++++++++++++++++ 3 files changed, 135 insertions(+) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index 3c7f87f9..7a8e2a20 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -163,7 +163,17 @@ No resources. | [tasks\_iam\_role\_tags](#input\_tasks\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | | [tasks\_iam\_role\_use\_name\_prefix](#input\_tasks\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`tasks_iam_role_name`) is used as a prefix | `bool` | `true` | no | | [terraform\_source](#input\_terraform\_source) | Source location to record in the Terraform\_source tag. Defaults to this module path. | `string` | `null` | no | +| [timeouts](#input\_timeouts) | Create, update, and delete timeout configurations for the service |
object({
create = optional(string)
delete = optional(string)
update = optional(string)
})
| `null` | no | | [tool](#input\_tool) | The tool used to deploy the resource | `string` | `"Terraform"` | no | +| [track\_latest](#input\_track\_latest) | Whether should track latest `ACTIVE` task definition on AWS or the one created with the resource stored in state. Useful in the event the task definition is modified outside of this resource | `bool` | `true` | no | +| [triggers](#input\_triggers) | Map of arbitrary keys and values that, when changed, will trigger an in-place update (redeployment). Useful with `timestamp()` | `map(string)` | `null` | no | +| [volume](#input\_volume) | Configuration block for volumes that containers in your task may use |
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)
}))
| `null` | no | +| [volume\_configuration](#input\_volume\_configuration) | Configuration for a volume specified in the task definition as a volume that is configured at launch time |
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_initialization_rate = optional(number)
volume_type = optional(string)
})
})
| `null` | no | +| [vpc\_id](#input\_vpc\_id) | The VPC ID where to deploy the task or service. If not provided, the VPC ID is derived from the subnets provided | `string` | `null` | no | +| [vpc\_lattice\_configurations](#input\_vpc\_lattice\_configurations) | The VPC Lattice configuration for your service that allows Lattice to connect, secure, and monitor your service across multiple accounts and VPCs |
object({
role_arn = string
target_group_arn = string
port_name = string
})
| `null` | no | +| [wait\_for\_steady\_state](#input\_wait\_for\_steady\_state) | If true, Terraform will wait for the service to reach a steady state before continuing. Default is `false` | `bool` | `null` | no | +| [wait\_until\_stable](#input\_wait\_until\_stable) | Whether terraform should wait until the task set has reached `STEADY_STATE` | `bool` | `null` | no | +| [wait\_until\_stable\_timeout](#input\_wait\_until\_stable\_timeout) | Wait timeout for task set to reach `STEADY_STATE`. Valid time units include `ns`, `us` (or µs), `ms`, `s`, `m`, and `h`. Default `10m` | `string` | `null` | no | | [workspace](#input\_workspace) | ID element. The Terraform workspace, to help ensure generated IDs are unique across workspaces | `string` | `null` | no | ## Outputs diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index 0fd21b71..9f98e545 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -111,4 +111,14 @@ module "ecs_service" { tasks_iam_role_statements = var.tasks_iam_role_statements tasks_iam_role_tags = var.tasks_iam_role_tags tasks_iam_role_use_name_prefix = var.tasks_iam_role_use_name_prefix + timeouts = var.timeouts + track_latest = var.track_latest + triggers = var.triggers + volume = var.volume + volume_configuration = var.volume_configuration + vpc_id = var.vpc_id + vpc_lattice_configurations = var.vpc_lattice_configurations + wait_for_steady_state = var.wait_for_steady_state + wait_until_stable = var.wait_until_stable + wait_until_stable_timeout = var.wait_until_stable_timeout } diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index 2007b9aa..78fc6bbc 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -1118,3 +1118,118 @@ variable "tasks_iam_role_use_name_prefix" { type = bool default = true } + +variable "timeouts" { + description = "Create, update, and delete timeout configurations for the service" + type = object({ + create = optional(string) + delete = optional(string) + update = optional(string) + }) + default = null +} + +variable "track_latest" { + description = "Whether should track latest `ACTIVE` task definition on AWS or the one created with the resource stored in state. Useful in the event the task definition is modified outside of this resource" + type = bool + default = true +} + +variable "triggers" { + description = "Map of arbitrary keys and values that, when changed, will trigger an in-place update (redeployment). Useful with `timestamp()`" + type = map(string) + default = null +} + +variable "volume" { + description = "Configuration block for volumes that containers in your task may use" + type = 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) + })) + default = null +} + +variable "volume_configuration" { + description = "Configuration for a volume specified in the task definition as a volume that is configured at launch time" + type = 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_initialization_rate = optional(number) + volume_type = optional(string) + }) + }) + default = null +} + +variable "vpc_id" { + description = "The VPC ID where to deploy the task or service. If not provided, the VPC ID is derived from the subnets provided" + type = string + default = null +} + +variable "vpc_lattice_configurations" { + description = "The VPC Lattice configuration for your service that allows Lattice to connect, secure, and monitor your service across multiple accounts and VPCs" + type = object({ + role_arn = string + target_group_arn = string + port_name = string + }) + default = null +} + +variable "wait_for_steady_state" { + description = "If true, Terraform will wait for the service to reach a steady state before continuing. Default is `false`" + type = bool + default = null +} + +variable "wait_until_stable" { + description = "Whether terraform should wait until the task set has reached `STEADY_STATE`" + type = bool + default = null +} + +variable "wait_until_stable_timeout" { + description = "Wait timeout for task set to reach `STEADY_STATE`. Valid time units include `ns`, `us` (or µs), `ms`, `s`, `m`, and `h`. Default `10m`" + type = string + default = null +} From 9545a7a7a2ded86ec2be1e0419ddd40cf89f5b64 Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 12:45:19 +0100 Subject: [PATCH 17/22] feat(ecs-service): add outputs --- infrastructure/modules/ecs-service/README.md | 28 +++- infrastructure/modules/ecs-service/outputs.tf | 125 +++++++++++++++++- 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index 7a8e2a20..d8895c6e 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -178,7 +178,33 @@ No resources. ## Outputs -No outputs. +| Name | Description | +| ---- | ----------- | +| [autoscaling\_policies](#output\_autoscaling\_policies) | Map of autoscaling policies and their attributes | +| [autoscaling\_scheduled\_actions](#output\_autoscaling\_scheduled\_actions) | Map of autoscaling scheduled actions and their attributes | +| [container\_definitions](#output\_container\_definitions) | Container definitions | +| [ecs\_service\_id](#output\_ecs\_service\_id) | ARN that identifies the service | +| [ecs\_service\_name](#output\_ecs\_service\_name) | Name of the service | +| [iam\_role\_arn](#output\_iam\_role\_arn) | Service IAM role ARN | +| [iam\_role\_name](#output\_iam\_role\_name) | Service IAM role name | +| [iam\_role\_unique\_id](#output\_iam\_role\_unique\_id) | Stable and unique string identifying the service IAM role | +| [infrastructure\_iam\_role\_arn](#output\_infrastructure\_iam\_role\_arn) | Infrastructure IAM role ARN | +| [infrastructure\_iam\_role\_name](#output\_infrastructure\_iam\_role\_name) | Infrastructure IAM role name | +| [security\_group\_arn](#output\_security\_group\_arn) | ARN of the security group | +| [security\_group\_id](#output\_security\_group\_id) | ID of the security group | +| [task\_definition\_arn](#output\_task\_definition\_arn) | Full ARN of the Task Definition (including both `family` and `revision`) | +| [task\_definition\_family](#output\_task\_definition\_family) | The unique name of the task definition | +| [task\_definition\_revision](#output\_task\_definition\_revision) | Revision of the task in a particular family | +| [task\_exec\_iam\_role\_arn](#output\_task\_exec\_iam\_role\_arn) | Task execution IAM role ARN | +| [task\_exec\_iam\_role\_name](#output\_task\_exec\_iam\_role\_name) | Task execution IAM role name | +| [task\_exec\_iam\_role\_unique\_id](#output\_task\_exec\_iam\_role\_unique\_id) | Stable and unique string identifying the task execution IAM role | +| [task\_set\_arn](#output\_task\_set\_arn) | The ARN that identifies the task set | +| [task\_set\_id](#output\_task\_set\_id) | The ID of the task set | +| [task\_set\_stability\_status](#output\_task\_set\_stability\_status) | The stability status. This indicates whether the task set has reached a steady state | +| [task\_set\_status](#output\_task\_set\_status) | The status of the task set | +| [tasks\_iam\_role\_arn](#output\_tasks\_iam\_role\_arn) | Tasks IAM role ARN | +| [tasks\_iam\_role\_name](#output\_tasks\_iam\_role\_name) | Tasks IAM role name | +| [tasks\_iam\_role\_unique\_id](#output\_tasks\_iam\_role\_unique\_id) | Stable and unique string identifying the tasks IAM role | diff --git a/infrastructure/modules/ecs-service/outputs.tf b/infrastructure/modules/ecs-service/outputs.tf index c9fb5240..45595c45 100644 --- a/infrastructure/modules/ecs-service/outputs.tf +++ b/infrastructure/modules/ecs-service/outputs.tf @@ -1 +1,124 @@ -# DAVEH +output "ecs_service_id" { + description = "ARN that identifies the service" + value = module.ecs_service.id +} + +output "ecs_service_name" { + description = "Name of the service" + value = module.ecs_service.name +} + +output "autoscaling_policies" { + description = "Map of autoscaling policies and their attributes" + value = module.ecs_service.autoscaling_policies +} + +output "autoscaling_scheduled_actions" { + description = "Map of autoscaling scheduled actions and their attributes" + value = module.ecs_service.autoscaling_scheduled_actions +} + +output "container_definitions" { + description = "Container definitions" + value = module.ecs_service.container_definitions +} + +output "iam_role_arn" { + description = "Service IAM role ARN" + value = module.ecs_service.iam_role_arn +} + +output "iam_role_name" { + description = "Service IAM role name" + value = module.ecs_service.iam_role_name +} + +output "iam_role_unique_id" { + description = "Stable and unique string identifying the service IAM role" + value = module.ecs_service.iam_role_unique_id +} + +output "infrastructure_iam_role_arn" { + description = "Infrastructure IAM role ARN" + value = module.ecs_service.infrastructure_iam_role_arn +} + +output "infrastructure_iam_role_name" { + description = "Infrastructure IAM role name" + value = module.ecs_service.infrastructure_iam_role_name +} + +output "security_group_arn" { + description = "ARN of the security group" + value = module.ecs_service.security_group_arn +} + +output "security_group_id" { + description = "ID of the security group" + value = module.ecs_service.security_group_id +} + +output "task_definition_arn" { + description = "Full ARN of the Task Definition (including both `family` and `revision`)" + value = module.ecs_service.task_definition_arn +} + +output "task_definition_family" { + description = "The unique name of the task definition" + value = module.ecs_service.task_definition_family +} + +output "task_definition_revision" { + description = "Revision of the task in a particular family" + value = module.ecs_service.task_definition_revision +} + +output "task_exec_iam_role_arn" { + description = "Task execution IAM role ARN" + value = module.ecs_service.task_exec_iam_role_arn +} + +output "task_exec_iam_role_name" { + description = "Task execution IAM role name" + value = module.ecs_service.task_exec_iam_role_name +} + +output "task_exec_iam_role_unique_id" { + description = "Stable and unique string identifying the task execution IAM role" + value = module.ecs_service.task_exec_iam_role_unique_id +} + +output "task_set_arn" { + description = "The ARN that identifies the task set" + value = module.ecs_service.task_set_arn +} + +output "task_set_id" { + description = "The ID of the task set" + value = module.ecs_service.task_set_id +} + +output "task_set_stability_status" { + description = "The stability status. This indicates whether the task set has reached a steady state" + value = module.ecs_service.task_set_stability_status +} + +output "task_set_status" { + description = "The status of the task set" + value = module.ecs_service.task_set_status +} + +output "tasks_iam_role_arn" { + description = "Tasks IAM role ARN" + value = module.ecs_service.tasks_iam_role_arn +} + +output "tasks_iam_role_name" { + description = "Tasks IAM role name" + value = module.ecs_service.tasks_iam_role_name +} + +output "tasks_iam_role_unique_id" { + description = "Stable and unique string identifying the tasks IAM role" + value = module.ecs_service.tasks_iam_role_unique_id +} From 5d0b1349ead8b70cb92084508ba9750e0808ffbd Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 12:47:46 +0100 Subject: [PATCH 18/22] docs(ecs-service): remove unnecessary comment --- infrastructure/modules/ecs-service/README.md | 2 +- infrastructure/modules/ecs-service/variables.tf | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index d8895c6e..993603fe 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -43,7 +43,7 @@ No resources. | [aws\_region](#input\_aws\_region) | The AWS region | `string` | `"eu-west-2"` | no | | [capacity\_provider\_strategy](#input\_capacity\_provider\_strategy) | Capacity provider strategies to use for the service. Can be one or more |
map(object({
base = optional(number)
capacity_provider = string
weight = optional(number)
}))
| `null` | no | | [cluster\_arn](#input\_cluster\_arn) | ARN of the ECS cluster where the resources will be provisioned | `string` | `""` | no | -| [container\_definitions](#input\_container\_definitions) | A map of valid container definitions . Please note that you should only provide values that are part of the container definition document |
map(object({
create = optional(bool, true)
operating_system_family = optional(string)
tags = optional(map(string)) # Container definition
command = optional(list(string))
cpu = optional(number)
credentialSpecs = optional(list(string))
dependsOn = optional(list(object({
condition = string
containerName = string
})))
disableNetworking = optional(bool)
dnsSearchDomains = optional(list(string))
dnsServers = optional(list(string))
dockerLabels = optional(map(string))
dockerSecurityOptions = optional(list(string))
# DAVEH: following line was comment to preceeding line
enable_execute_command = optional(bool, false) # Set in standalone variable
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, 30)
retries = optional(number, 3)
startPeriod = optional(number)
timeout = optional(number, 5)
}))
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, 30)
stopTimeout = optional(number, 120)
systemControls = optional(list(object({
namespace = optional(string)
value = optional(string)
})))
ulimits = optional(list(object({
hardLimit = number
name = string
softLimit = number
})))
user = optional(string)
versionConsistency = optional(string)
volumesFrom = optional(list(object({
readOnly = optional(bool)
sourceContainer = optional(string)
})))
workingDirectory = optional(string)
# Cloudwatch Log Group
service = optional(string)
enable_cloudwatch_logging = optional(bool)
create_cloudwatch_log_group = optional(bool)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool)
cloudwatch_log_group_class = optional(string)
cloudwatch_log_group_retention_in_days = optional(number)
cloudwatch_log_group_kms_key_id = optional(string)
}))
| `{}` | no | +| [container\_definitions](#input\_container\_definitions) | A map of valid container definitions . Please note that you should only provide values that are part of the container definition document |
map(object({
create = optional(bool, true)
operating_system_family = optional(string)
tags = optional(map(string)) # Container definition
command = optional(list(string))
cpu = optional(number)
credentialSpecs = optional(list(string))
dependsOn = optional(list(object({
condition = string
containerName = string
})))
disableNetworking = optional(bool)
dnsSearchDomains = optional(list(string))
dnsServers = optional(list(string))
dockerLabels = optional(map(string))
dockerSecurityOptions = optional(list(string))
enable_execute_command = optional(bool, false) # Set in standalone variable
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, 30)
retries = optional(number, 3)
startPeriod = optional(number)
timeout = optional(number, 5)
}))
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, 30)
stopTimeout = optional(number, 120)
systemControls = optional(list(object({
namespace = optional(string)
value = optional(string)
})))
ulimits = optional(list(object({
hardLimit = number
name = string
softLimit = number
})))
user = optional(string)
versionConsistency = optional(string)
volumesFrom = optional(list(object({
readOnly = optional(bool)
sourceContainer = optional(string)
})))
workingDirectory = optional(string)
# Cloudwatch Log Group
service = optional(string)
enable_cloudwatch_logging = optional(bool)
create_cloudwatch_log_group = optional(bool)
cloudwatch_log_group_name = optional(string)
cloudwatch_log_group_use_name_prefix = optional(bool)
cloudwatch_log_group_class = optional(string)
cloudwatch_log_group_retention_in_days = optional(number)
cloudwatch_log_group_kms_key_id = optional(string)
}))
| `{}` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"project": null,
"regex_replace_chars": null,
"region": null,
"service": null,
"stack": null,
"tags": {},
"terraform_source": null,
"workspace": null
}
| no | | [cpu](#input\_cpu) | Number of cpu units used by the task. If the `requires_compatibilities` is `FARGATE` this field is required | `number` | `1024` | no | | [create\_iam\_role](#input\_create\_iam\_role) | Determines whether the ECS service IAM role should be created | `bool` | `true` | no | diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index 78fc6bbc..bb6f8fa8 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -241,12 +241,11 @@ variable "container_definitions" { condition = string containerName = string }))) - disableNetworking = optional(bool) - dnsSearchDomains = optional(list(string)) - dnsServers = optional(list(string)) - dockerLabels = optional(map(string)) - dockerSecurityOptions = optional(list(string)) - # DAVEH: following line was comment to preceeding line + disableNetworking = optional(bool) + dnsSearchDomains = optional(list(string)) + dnsServers = optional(list(string)) + dockerLabels = optional(map(string)) + dockerSecurityOptions = optional(list(string)) enable_execute_command = optional(bool, false) # Set in standalone variable entrypoint = optional(list(string)) environment = optional(list(object({ From 1f4f5329c65d905f9fcc3b604e94a73cb45f7439 Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 13:41:41 +0100 Subject: [PATCH 19/22] docs(ecs-service): write intro --- infrastructure/modules/ecs-service/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index 993603fe..3e0e38cd 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -1,4 +1,8 @@ -# DAVEH +# ECS-Service + +NHS Screening wrapper around the community +[`terraform-aws-modules/ecs/aws//modules/service`](https://registry.terraform.io/modules/terraform-aws-modules/ecs/aws/latest/submodules/service) +submodule that consumes the shared `context.tf` for naming and tagging. From 509cf51755e6567b15aca8511078885e98890930 Mon Sep 17 00:00:00 2001 From: Dave Hinton Date: Mon, 22 Jun 2026 13:45:08 +0100 Subject: [PATCH 20/22] docs(ecs-service): write examples --- infrastructure/modules/ecs-service/README.md | 106 +++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index 3e0e38cd..b24505c1 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -4,6 +4,112 @@ NHS Screening wrapper around the community [`terraform-aws-modules/ecs/aws//modules/service`](https://registry.terraform.io/modules/terraform-aws-modules/ecs/aws/latest/submodules/service) submodule that consumes the shared `context.tf` for naming and tagging. +## Usage + +### Minimal Fargate service + +```hcl +module "api_service" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=main" + + service = "bcss" + project = "api" + environment = "development" + name = "web-api" + cluster_arn = module.ecs_cluster.arn + subnet_ids = module.vpc.private_subnets + + container_definitions = { + app = { + image = "my-registry.dkr.ecr.eu-west-2.amazonaws.com/my-app:latest" + cpu = 512 + memory = 1024 + essential = true + } + } +} +``` + +### Fargate service with load balancer + +```hcl +module "web_service" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=main" + + service = "bcss" + project = "web" + environment = "prod" + name = "frontend" + cluster_arn = module.ecs_cluster.arn + subnet_ids = module.vpc.private_subnets + assign_public_ip = false + + container_definitions = { + web = { + image = "my-registry.dkr.ecr.eu-west-2.amazonaws.com/web-app:v1.2.3" + cpu = 512 + memory = 1024 + essential = true + port_mappings = [{ + container_port = 8080 + host_port = 8080 + }] + } + } + + load_balancer = { + web = { + container_name = "web" + container_port = 8080 + target_group_arn = module.alb.target_group_arn + } + } + + health_check_grace_period_seconds = 60 +} +``` + +### Fargate service with autoscaling + +```hcl +module "worker_service" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=main" + + service = "bcss" + project = "jobs" + environment = "prod" + name = "background-worker" + cluster_arn = module.ecs_cluster.arn + subnet_ids = module.vpc.private_subnets + + container_definitions = { + worker = { + image = "my-registry.dkr.ecr.eu-west-2.amazonaws.com/worker:latest" + cpu = 256 + memory = 512 + essential = true + } + } + + desired_count = 2 + enable_autoscaling = true + autoscaling_min_capacity = 2 + autoscaling_max_capacity = 10 + + autoscaling_policies = { + cpu = { + policy_type = "TargetTrackingScaling" + target_tracking_scaling_policy_configuration = { + target_value = 70.0 + predefined_metric_specification = { + predefined_metric_type = "ECSServiceAverageCPUUtilization" + } + } + } + } +} +``` + From 0d4df63fef7f11a3d7adf48534a7191bd15b1a86 Mon Sep 17 00:00:00 2001 From: Oliver Slater Date: Mon, 22 Jun 2026 16:48:08 +0100 Subject: [PATCH 21/22] feat(ecs-service): enhance README and add locals for service name management --- infrastructure/modules/ecs-service/README.md | 196 ++++++++++++++++-- infrastructure/modules/ecs-service/locals.tf | 6 + infrastructure/modules/ecs-service/main.tf | 20 +- .../modules/ecs-service/variables.tf | 13 ++ 4 files changed, 214 insertions(+), 21 deletions(-) create mode 100644 infrastructure/modules/ecs-service/locals.tf diff --git a/infrastructure/modules/ecs-service/README.md b/infrastructure/modules/ecs-service/README.md index b24505c1..98a686e5 100644 --- a/infrastructure/modules/ecs-service/README.md +++ b/infrastructure/modules/ecs-service/README.md @@ -1,8 +1,20 @@ -# ECS-Service +# ECS Service NHS Screening wrapper around the community [`terraform-aws-modules/ecs/aws//modules/service`](https://registry.terraform.io/modules/terraform-aws-modules/ecs/aws/latest/submodules/service) -submodule that consumes the shared `context.tf` for naming and tagging. +submodule that enforces the platform's baseline controls and consumes +the shared `context.tf` for naming and tagging. + +## What this module enforces + +|Control|How it is enforced| +|---|---| +|No public IP|`assign_public_ip` defaults to `false`; tasks run on private subnets| +|ECS-managed tags|`enable_ecs_managed_tags` defaults to `true`; AWS propagates resource tags to tasks| +|Tag propagation|`propagate_tags` defaults to `TASK_DEFINITION` so tasks inherit service tags| +|Creation gate|`create = module.this.enabled`; no resources are created when the module is disabled| +|Consistent naming|Service name is sourced from `local.service_name` (context-derived or `var.service_name` override)| +|Consistent tagging|All resources tagged via `module.this.tags`| ## Usage @@ -10,7 +22,7 @@ submodule that consumes the shared `context.tf` for naming and tagging. ```hcl module "api_service" { - source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=main" + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=" service = "bcss" project = "api" @@ -34,15 +46,16 @@ module "api_service" { ```hcl module "web_service" { - source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=main" + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=" - service = "bcss" - project = "web" - environment = "prod" - name = "frontend" - cluster_arn = module.ecs_cluster.arn - subnet_ids = module.vpc.private_subnets - assign_public_ip = false + service = "bcss" + project = "web" + environment = "prod" + name = "frontend" + + cluster_arn = module.ecs_cluster.arn + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets container_definitions = { web = { @@ -73,14 +86,16 @@ module "web_service" { ```hcl module "worker_service" { - source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=main" + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=" - service = "bcss" - project = "jobs" - environment = "prod" - name = "background-worker" - cluster_arn = module.ecs_cluster.arn - subnet_ids = module.vpc.private_subnets + service = "bcss" + project = "jobs" + environment = "prod" + name = "background-worker" + + cluster_arn = module.ecs_cluster.arn + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets container_definitions = { worker = { @@ -110,6 +125,148 @@ module "worker_service" { } ``` +### Service reading secrets from Secrets Manager + +```hcl +module "api_service" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=" + + service = "bcss" + project = "api" + environment = "prod" + name = "web-api" + + cluster_arn = module.ecs_cluster.arn + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + + # Grant the task execution role permission to read these secrets. + # The values are injected into the container at runtime via the + # secrets block in the container definition below. + task_exec_secret_arns = [ + module.db_credentials.secret_arn, + module.api_key.secret_arn, + ] + + container_definitions = { + app = { + image = "my-registry.dkr.ecr.eu-west-2.amazonaws.com/my-app:latest" + cpu = 512 + memory = 1024 + essential = true + + secrets = [ + { + name = "DB_PASSWORD" + valueFrom = module.db_credentials.secret_arn + }, + { + name = "API_KEY" + valueFrom = module.api_key.secret_arn + }, + ] + } + } +} +``` + +### Blue/green deployment + +```hcl +module "api_service" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=" + + service = "bcss" + project = "api" + environment = "prod" + name = "web-api" + + cluster_arn = module.ecs_cluster.arn + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + + deployment_configuration = { + strategy = "BLUE_GREEN" + bake_time_in_minutes = 5 + } + + deployment_circuit_breaker = { + enable = true + rollback = true + } + + container_definitions = { + app = { + image = "my-registry.dkr.ecr.eu-west-2.amazonaws.com/my-app:latest" + cpu = 512 + memory = 1024 + essential = true + } + } +} +``` + +### Preserving an existing service name + +Use `service_name` when you wish to explicitly define a service name i.e. when the ECS service was previously created outside Terraform and the name must be kept stable: + +```hcl +module "legacy_service" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=" + + service = "bcss" + project = "api" + environment = "prod" + name = "web-api" + + service_name = "bcss-legacy-worker" # overrides the context-derived name + + cluster_arn = module.ecs_cluster.arn + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + + container_definitions = { + app = { + image = "my-registry.dkr.ecr.eu-west-2.amazonaws.com/my-app:latest" + cpu = 256 + memory = 512 + essential = true + } + } +} +``` + +## Conventions + +- Service name defaults to `module.this.name` (derived from `context.tf`). Supply + `service`, `project`, `environment`, and `name` inputs to control the generated + name. Set `service_name` to override the context-derived name with an explicit + value — useful when an existing ECS service name must be preserved. +- `assign_public_ip` defaults to `false`. Only set it to `true` for public-facing + Fargate services that intentionally require direct internet access; this is rare + in the NHS Screening platform. +- `enable_autoscaling` defaults to `true`. Set it to `false` for batch or + short-lived services that do not require auto-scaling. +- `desired_count` defaults to `1`. Adjust to match your workload requirements. +- The caller must supply the ECS cluster ARN (`cluster_arn`) and the VPC subnet + IDs (`subnet_ids`). These are not managed by this module. + +## What this module does NOT do + +- Create an ECS cluster. Use the `ecs-cluster` module and pass the ARN via + `cluster_arn`. +- Create VPCs, subnets, or security groups. The caller is responsible for + networking; pass existing security group IDs via `security_group_ids` or let + the module create one via `create_security_group = true`. +- Create load balancers or target groups. Configure these separately and pass + the target group ARN(s) via `load_balancer`. +- Build or push container images. Use the `ecr` module for the registry. +- Manage KMS encryption at the ECS service level. Task-level secrets encryption + is handled via the task execution IAM role and the `secrets-manager` or + `parameter_store` modules. +- Create CloudWatch log groups. Define these in your container definitions or + provision them separately. + @@ -128,7 +285,7 @@ No providers. | Name | Source | Version | | ---- | ------ | ------- | -| [ecs\_service](#module\_ecs\_service) | terraform-aws-modules/ecs/aws//modules/service | ~> 7.5.0 | +| [ecs\_service](#module\_ecs\_service) | terraform-aws-modules/ecs/aws//modules/service | 7.5.0 | | [this](#module\_this) | ../tags | n/a | ## Resources @@ -238,6 +395,7 @@ No resources. | [service](#input\_service) | ID element. Usually an abbreviation of your service directorate name, e.g. 'bcss' or 'csms', to help ensure generated IDs are globally unique | `string` | `null` | no | | [service\_category](#input\_service\_category) | The tag service\_category | `string` | `"n/a"` | 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)
access_log_configuration = optional(object({
format = string
include_query_parameters = optional(string)
}))
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\_name](#input\_service\_name) | Name of the service | `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 | | [sigint\_rollback](#input\_sigint\_rollback) | Whether to enable graceful termination of deployments using SIGINT signals. Only applicable when using ECS deployment controller and requires wait\_for\_steady\_state = true. Default is false | `bool` | `null` | no | diff --git a/infrastructure/modules/ecs-service/locals.tf b/infrastructure/modules/ecs-service/locals.tf new file mode 100644 index 00000000..4ff52fa3 --- /dev/null +++ b/infrastructure/modules/ecs-service/locals.tf @@ -0,0 +1,6 @@ +locals { + # Service name is derived from context. The community module receives + # module.this.name directly so that context-driven label ordering is + # preserved without an intermediate local in the common case. + service_name = var.service_name != null ? var.service_name : module.this.name +} diff --git a/infrastructure/modules/ecs-service/main.tf b/infrastructure/modules/ecs-service/main.tf index 9f98e545..054ea646 100644 --- a/infrastructure/modules/ecs-service/main.tf +++ b/infrastructure/modules/ecs-service/main.tf @@ -1,9 +1,25 @@ +################################################################ +# ECS Service +# +# Thin NHS wrapper around the community ECS service submodule +# (terraform-aws-modules/ecs/aws//modules/service) that +# enforces the screening platform's baseline controls: +# +# * No public IP: assign_public_ip defaults to false +# * ECS-managed tags: enable_ecs_managed_tags defaults to true +# * Tag propagation: propagate_tags defaults to TASK_DEFINITION +# * Creation gated by module.this.enabled +# +# Naming: context.tf via module.this by default; caller may override +# with var.service_name. Tagging is always via module.this.tags. +################################################################ + module "ecs_service" { source = "terraform-aws-modules/ecs/aws//modules/service" - version = "~> 7.5.0" + version = "7.5.0" create = module.this.enabled - name = module.this.name + name = local.service_name tags = module.this.tags alarms = var.alarms diff --git a/infrastructure/modules/ecs-service/variables.tf b/infrastructure/modules/ecs-service/variables.tf index bb6f8fa8..bdb6ed0c 100644 --- a/infrastructure/modules/ecs-service/variables.tf +++ b/infrastructure/modules/ecs-service/variables.tf @@ -1,3 +1,10 @@ +################################################################ +# ECS Service-specific inputs. +# +# Naming, tagging and the master `enabled` switch come from +# context.tf via `module.this`. +################################################################ + variable "alarms" { description = "Information about the CloudWatch alarms" type = object({ @@ -884,6 +891,12 @@ variable "service_connect_configuration" { default = null } +variable "service_name" { + description = "Name of the service" + type = string + default = null +} + variable "service_registries" { description = "Service discovery registries for the service" type = object({ From 990fe1ca3cd056da2c6295817f070feceeab7257 Mon Sep 17 00:00:00 2001 From: Oliver Slater Date: Tue, 23 Jun 2026 08:55:05 +0100 Subject: [PATCH 22/22] feat(gitleaks): enhance gitleaks configuration for IPv4 and IPv6 rules --- .gitleaksignore | 4 ++++ scripts/config/gitleaks.toml | 27 +++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.gitleaksignore b/.gitleaksignore index f97f5c8a..bf9d628a 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -5,3 +5,7 @@ e876843351a025eb754ec61982c8b7d95deeb709:.pre-commit-config.yaml:ipv4:119 e364bc1869c67729653c7efb4d6169f2294e68de:.pre-commit-config.yaml:ipv4:110 62088509f98ce02ce379adef2168b867eecfb5da:.pre-commit-config.yaml:ipv4:110 a3fa25da4e8f9eaa2e28c29f6196f23bfe87a58d:.pre-commit-config.yaml:ipv4:119 +# Historical false positive: example ARN comment in tags/main.tf contained hex-like content +# which triggered the ipv6 rule. Comment updated in later commit; old commits suppressed here. +7b49758d98757e8f404cb2c540c1f146afd6e395:infrastructure/modules/tags/main.tf:ipv6:131 +091dcd76884ffd307aee6c6b306b015c065f4896:infrastructure/modules/tags/main.tf:ipv6:131 diff --git a/scripts/config/gitleaks.toml b/scripts/config/gitleaks.toml index af5f0bb7..8371dcbc 100644 --- a/scripts/config/gitleaks.toml +++ b/scripts/config/gitleaks.toml @@ -11,8 +11,31 @@ regex = '''[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}''' [rules.allowlist] regexTarget = "match" regexes = [ - # Exclude the private network IPv4 addresses as well as the DNS servers for Google and OpenDNS - '''(127\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|172\.(1[6-9]|2[0-9]|3[0-1])\.[0-9]{1,3}\.[0-9]{1,3}|192\.168\.[0-9]{1,3}\.[0-9]{1,3}|0\.0\.0\.0|255\.255\.255\.255|8\.8\.8\.8|8\.8\.4\.4|208\.67\.222\.222|208\.67\.220\.220)''', + # Exclude private/reserved IPv4 addresses and well-known DNS servers used in docs/examples. + # Includes RFC5737 TEST-NET ranges: 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 + '''(127\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|172\.(1[6-9]|2[0-9]|3[0-1])\.[0-9]{1,3}\.[0-9]{1,3}|192\.168\.[0-9]{1,3}\.[0-9]{1,3}|192\.0\.2\.[0-9]{1,3}|198\.51\.100\.[0-9]{1,3}|203\.0\.113\.[0-9]{1,3}|0\.0\.0\.0|255\.255\.255\.255|8\.8\.8\.8|8\.8\.4\.4|1\.1\.1\.1|1\.0\.0\.1)''', +] + +[[rules]] +description = "IPv6" +id = "ipv6" +# Matches valid IPv6 forms requiring at least 2 groups on each side of :: to +# avoid false positives from AWS ARNs (which use :: between region and account). +# full: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 +# compressed: 2001:db8::1, fe80:db8::1 +# trailing :: fe80:db8:: (2+ groups required before ::) +# leading :: ::db8:1 (2+ groups required after ::) +# Note: RE2 does not support lookahead/lookbehind so boundary enforcement is +# achieved structurally via minimum repetition counts. +regex = '''(?i)(?:[0-9a-f]{1,4}:){7}[0-9a-f]{1,4}|(?:[0-9a-f]{1,4}:){2,7}:|(?:[0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2}|(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3}|(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4}|(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:(?::[0-9a-f]{1,4}){1,6}|:(?::[0-9a-f]{1,4}){2,7}''' + +[rules.allowlist] +regexTarget = "match" +regexes = [ + # Exclude IPv6 documentation prefixes used in examples. + # RFC3849: 2001:db8::/32 + # RFC9637: 3fff::/20 (3fff:0000:: to 3fff:0fff::) + '''(?i)(^|[^0-9a-f])(2001:db8:|3fff:0[0-9a-f]{0,3}:)''', ] [allowlist]