Skip to content

Commit fe0f272

Browse files
feat(ecs-service): enhance README and add locals for service name management
1 parent 231dc8a commit fe0f272

5 files changed

Lines changed: 219 additions & 25 deletions

File tree

infrastructure/modules/ecs-service/README.md

Lines changed: 177 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
1-
# ECS-Service
1+
# ECS Service
22

33
NHS Screening wrapper around the community
44
[`terraform-aws-modules/ecs/aws//modules/service`](https://registry.terraform.io/modules/terraform-aws-modules/ecs/aws/latest/submodules/service)
5-
submodule that consumes the shared `context.tf` for naming and tagging.
5+
submodule that enforces the platform's baseline controls and consumes
6+
the shared `context.tf` for naming and tagging.
7+
8+
## What this module enforces
9+
10+
|Control|How it is enforced|
11+
|---|---|
12+
|No public IP|`assign_public_ip` defaults to `false`; tasks run on private subnets|
13+
|ECS-managed tags|`enable_ecs_managed_tags` defaults to `true`; AWS propagates resource tags to tasks|
14+
|Tag propagation|`propagate_tags` defaults to `TASK_DEFINITION` so tasks inherit service tags|
15+
|Creation gate|`create = module.this.enabled`; no resources are created when the module is disabled|
16+
|Consistent naming|Service name is sourced from `local.service_name` (context-derived or `var.service_name` override)|
17+
|Consistent tagging|All resources tagged via `module.this.tags`|
618

719
## Usage
820

921
### Minimal Fargate service
1022

1123
```hcl
1224
module "api_service" {
13-
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=main"
25+
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=<tag>"
1426
1527
service = "bcss"
1628
project = "api"
@@ -34,15 +46,16 @@ module "api_service" {
3446

3547
```hcl
3648
module "web_service" {
37-
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=main"
49+
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=<tag>"
3850
39-
service = "bcss"
40-
project = "web"
41-
environment = "prod"
42-
name = "frontend"
43-
cluster_arn = module.ecs_cluster.arn
44-
subnet_ids = module.vpc.private_subnets
45-
assign_public_ip = false
51+
service = "bcss"
52+
project = "web"
53+
environment = "prod"
54+
name = "frontend"
55+
56+
cluster_arn = module.ecs_cluster.arn
57+
vpc_id = module.vpc.vpc_id
58+
subnet_ids = module.vpc.private_subnets
4659
4760
container_definitions = {
4861
web = {
@@ -73,14 +86,16 @@ module "web_service" {
7386

7487
```hcl
7588
module "worker_service" {
76-
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=main"
89+
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=<tag>"
7790
78-
service = "bcss"
79-
project = "jobs"
80-
environment = "prod"
81-
name = "background-worker"
82-
cluster_arn = module.ecs_cluster.arn
83-
subnet_ids = module.vpc.private_subnets
91+
service = "bcss"
92+
project = "jobs"
93+
environment = "prod"
94+
name = "background-worker"
95+
96+
cluster_arn = module.ecs_cluster.arn
97+
vpc_id = module.vpc.vpc_id
98+
subnet_ids = module.vpc.private_subnets
8499
85100
container_definitions = {
86101
worker = {
@@ -110,6 +125,148 @@ module "worker_service" {
110125
}
111126
```
112127

128+
### Service reading secrets from Secrets Manager
129+
130+
```hcl
131+
module "api_service" {
132+
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=<tag>"
133+
134+
service = "bcss"
135+
project = "api"
136+
environment = "prod"
137+
name = "web-api"
138+
139+
cluster_arn = module.ecs_cluster.arn
140+
vpc_id = module.vpc.vpc_id
141+
subnet_ids = module.vpc.private_subnets
142+
143+
# Grant the task execution role permission to read these secrets.
144+
# The values are injected into the container at runtime via the
145+
# secrets block in the container definition below.
146+
task_exec_secret_arns = [
147+
module.db_credentials.secret_arn,
148+
module.api_key.secret_arn,
149+
]
150+
151+
container_definitions = {
152+
app = {
153+
image = "my-registry.dkr.ecr.eu-west-2.amazonaws.com/my-app:latest"
154+
cpu = 512
155+
memory = 1024
156+
essential = true
157+
158+
secrets = [
159+
{
160+
name = "DB_PASSWORD"
161+
valueFrom = module.db_credentials.secret_arn
162+
},
163+
{
164+
name = "API_KEY"
165+
valueFrom = module.api_key.secret_arn
166+
},
167+
]
168+
}
169+
}
170+
}
171+
```
172+
173+
### Blue/green deployment
174+
175+
```hcl
176+
module "api_service" {
177+
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=<tag>"
178+
179+
service = "bcss"
180+
project = "api"
181+
environment = "prod"
182+
name = "web-api"
183+
184+
cluster_arn = module.ecs_cluster.arn
185+
vpc_id = module.vpc.vpc_id
186+
subnet_ids = module.vpc.private_subnets
187+
188+
deployment_configuration = {
189+
strategy = "BLUE_GREEN"
190+
bake_time_in_minutes = 5
191+
}
192+
193+
deployment_circuit_breaker = {
194+
enable = true
195+
rollback = true
196+
}
197+
198+
container_definitions = {
199+
app = {
200+
image = "my-registry.dkr.ecr.eu-west-2.amazonaws.com/my-app:latest"
201+
cpu = 512
202+
memory = 1024
203+
essential = true
204+
}
205+
}
206+
}
207+
```
208+
209+
### Preserving an existing service name
210+
211+
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:
212+
213+
```hcl
214+
module "legacy_service" {
215+
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/ecs-service?ref=<tag>"
216+
217+
service = "bcss"
218+
project = "api"
219+
environment = "prod"
220+
name = "web-api"
221+
222+
service_name = "bcss-legacy-worker" # overrides the context-derived name
223+
224+
cluster_arn = module.ecs_cluster.arn
225+
vpc_id = module.vpc.vpc_id
226+
subnet_ids = module.vpc.private_subnets
227+
228+
container_definitions = {
229+
app = {
230+
image = "my-registry.dkr.ecr.eu-west-2.amazonaws.com/my-app:latest"
231+
cpu = 256
232+
memory = 512
233+
essential = true
234+
}
235+
}
236+
}
237+
```
238+
239+
## Conventions
240+
241+
- Service name defaults to `module.this.name` (derived from `context.tf`). Supply
242+
`service`, `project`, `environment`, and `name` inputs to control the generated
243+
name. Set `service_name` to override the context-derived name with an explicit
244+
value — useful when an existing ECS service name must be preserved.
245+
- `assign_public_ip` defaults to `false`. Only set it to `true` for public-facing
246+
Fargate services that intentionally require direct internet access; this is rare
247+
in the NHS Screening platform.
248+
- `enable_autoscaling` defaults to `true`. Set it to `false` for batch or
249+
short-lived services that do not require auto-scaling.
250+
- `desired_count` defaults to `1`. Adjust to match your workload requirements.
251+
- The caller must supply the ECS cluster ARN (`cluster_arn`) and the VPC subnet
252+
IDs (`subnet_ids`). These are not managed by this module.
253+
254+
## What this module does NOT do
255+
256+
- Create an ECS cluster. Use the `ecs-cluster` module and pass the ARN via
257+
`cluster_arn`.
258+
- Create VPCs, subnets, or security groups. The caller is responsible for
259+
networking; pass existing security group IDs via `security_group_ids` or let
260+
the module create one via `create_security_group = true`.
261+
- Create load balancers or target groups. Configure these separately and pass
262+
the target group ARN(s) via `load_balancer`.
263+
- Build or push container images. Use the `ecr` module for the registry.
264+
- Manage KMS encryption at the ECS service level. Task-level secrets encryption
265+
is handled via the task execution IAM role and the `secrets-manager` or
266+
`parameter_store` modules.
267+
- Create CloudWatch log groups. Define these in your container definitions or
268+
provision them separately.
269+
113270
<!-- vale off -->
114271
<!-- markdownlint-disable -->
115272
<!-- BEGIN_TF_DOCS -->
@@ -128,7 +285,7 @@ No providers.
128285

129286
| Name | Source | Version |
130287
| ---- | ------ | ------- |
131-
| <a name="module_ecs_service"></a> [ecs\_service](#module\_ecs\_service) | terraform-aws-modules/ecs/aws//modules/service | ~> 7.5.0 |
288+
| <a name="module_ecs_service"></a> [ecs\_service](#module\_ecs\_service) | terraform-aws-modules/ecs/aws//modules/service | 7.5.0 |
132289
| <a name="module_this"></a> [this](#module\_this) | ../tags | n/a |
133290

134291
## Resources
@@ -238,6 +395,7 @@ No resources.
238395
| <a name="input_service"></a> [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 |
239396
| <a name="input_service_category"></a> [service\_category](#input\_service\_category) | The tag service\_category | `string` | `"n/a"` | no |
240397
| <a name="input_service_connect_configuration"></a> [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 | <pre>object({<br/> enabled = optional(bool, true)<br/> access_log_configuration = optional(object({<br/> format = string<br/> include_query_parameters = optional(string)<br/> }))<br/> log_configuration = optional(object({<br/> log_driver = string<br/> options = optional(map(string))<br/> secret_option = optional(list(object({<br/> name = string<br/> value_from = string<br/> })))<br/> }))<br/> namespace = optional(string)<br/> service = optional(list(object({<br/> client_alias = optional(object({<br/> dns_name = optional(string)<br/> port = number<br/> test_traffic_rules = optional(list(object({<br/> header = optional(object({<br/> name = string<br/> value = object({<br/> exact = string<br/> })<br/> }))<br/> })))<br/> }))<br/> discovery_name = optional(string)<br/> ingress_port_override = optional(number)<br/> port_name = string<br/> timeout = optional(object({<br/> idle_timeout_seconds = optional(number)<br/> per_request_timeout_seconds = optional(number)<br/> }))<br/> tls = optional(object({<br/> issuer_cert_authority = object({<br/> aws_pca_authority_arn = string<br/> })<br/> kms_key = optional(string)<br/> role_arn = optional(string)<br/> }))<br/> })))<br/> })</pre> | `null` | no |
398+
| <a name="input_service_name"></a> [service\_name](#input\_service\_name) | Name of the service | `string` | `null` | no |
241399
| <a name="input_service_registries"></a> [service\_registries](#input\_service\_registries) | Service discovery registries for the service | <pre>object({<br/> container_name = optional(string)<br/> container_port = optional(number)<br/> port = optional(number)<br/> registry_arn = string<br/> })</pre> | `null` | no |
242400
| <a name="input_service_tags"></a> [service\_tags](#input\_service\_tags) | A map of additional tags to add to the service | `map(string)` | `{}` | no |
243401
| <a name="input_sigint_rollback"></a> [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 |
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
locals {
2+
# Service name is derived from context. The community module receives
3+
# module.this.name directly so that context-driven label ordering is
4+
# preserved without an intermediate local in the common case.
5+
service_name = var.service_name != null ? var.service_name : module.this.name
6+
}

infrastructure/modules/ecs-service/main.tf

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
1+
################################################################
2+
# ECS Service
3+
#
4+
# Thin NHS wrapper around the community ECS service submodule
5+
# (terraform-aws-modules/ecs/aws//modules/service) that
6+
# enforces the screening platform's baseline controls:
7+
#
8+
# * No public IP: assign_public_ip defaults to false
9+
# * ECS-managed tags: enable_ecs_managed_tags defaults to true
10+
# * Tag propagation: propagate_tags defaults to TASK_DEFINITION
11+
# * Creation gated by module.this.enabled
12+
#
13+
# Naming: context.tf via module.this by default; caller may override
14+
# with var.service_name. Tagging is always via module.this.tags.
15+
################################################################
16+
117
module "ecs_service" {
218
source = "terraform-aws-modules/ecs/aws//modules/service"
3-
version = "~> 7.5.0"
19+
version = "7.5.0"
420

521
create = module.this.enabled
6-
name = module.this.name
22+
name = local.service_name
723
tags = module.this.tags
824

925
alarms = var.alarms

infrastructure/modules/ecs-service/variables.tf

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
################################################################
2+
# ECS Service-specific inputs.
3+
#
4+
# Naming, tagging and the master `enabled` switch come from
5+
# context.tf via `module.this`.
6+
################################################################
7+
18
variable "alarms" {
29
description = "Information about the CloudWatch alarms"
310
type = object({
@@ -884,6 +891,12 @@ variable "service_connect_configuration" {
884891
default = null
885892
}
886893

894+
variable "service_name" {
895+
description = "Name of the service"
896+
type = string
897+
default = null
898+
}
899+
887900
variable "service_registries" {
888901
description = "Service discovery registries for the service"
889902
type = object({

scripts/config/vale/styles/config/vocabularies/words/accept.txt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ account_id
77
actionlint
88
api_gateway_name
99
api_path_part
10-
arn
10+
[Aa][Rr][Nn]
1111
backup_copy_vault_account_id
1212
backup_vault_name
1313
batch_id
@@ -42,8 +42,8 @@ handler_prefix
4242
healthcheck
4343
http_method
4444
idempotence
45-
iam
46-
itterate
45+
[Ii][Aa][Mm]
46+
[Ii]ntra
4747
Jira
4848
jq
4949
jQuery
@@ -85,7 +85,7 @@ shortcode
8585
source_account_name
8686
stage_name
8787
start_time
88-
subnet
88+
[Ss]ubnet
8989
Syft
9090
[Mm]arkdownlint
9191
[Tt]eardown
@@ -101,5 +101,6 @@ URL
101101
url
102102
vault_lock_type
103103
vault_name
104+
[Vv][Pp][Cc]
104105
[Uu][Uu][Ii][Dd]
105106
yq

0 commit comments

Comments
 (0)