From 4e9e9d5c6c6751845138e598b3276b858732afef Mon Sep 17 00:00:00 2001 From: Sean Rankine Date: Fri, 20 Feb 2026 12:04:10 +0000 Subject: [PATCH 1/3] Add common role to run ECS Scheduled Events This is a shared IAM role for ECS events, this is needed by EventBridge to run ECS scheduled tasks. --- infra/modules/environment/ecs.tf | 26 ++++++++++++++++++++++++++ infra/modules/environment/outputs.tf | 4 ++++ 2 files changed, 30 insertions(+) diff --git a/infra/modules/environment/ecs.tf b/infra/modules/environment/ecs.tf index d032917eb..23a7ff405 100644 --- a/infra/modules/environment/ecs.tf +++ b/infra/modules/environment/ecs.tf @@ -6,3 +6,29 @@ resource "aws_ecs_cluster" "forms" { value = "enhanced" } } + + +## ECS Events Role +## This a common role used by EventBridge to run ECS tasks. Only needs to be created once per account. + +data "aws_iam_policy_document" "ecs_events_assume_role" { + statement { + actions = ["sts:AssumeRole"] + effect = "Allow" + + principals { + type = "Service" + identifiers = ["events.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "ecs_events" { + name = "ecsEventsRole" + assume_role_policy = data.aws_iam_policy_document.ecs_events_assume_role.json +} + +resource "aws_iam_role_policy_attachment" "ecs_events_policy" { + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole" + role = aws_iam_role.ecs_events.name +} diff --git a/infra/modules/environment/outputs.tf b/infra/modules/environment/outputs.tf index e48ae1599..403b5d568 100644 --- a/infra/modules/environment/outputs.tf +++ b/infra/modules/environment/outputs.tf @@ -106,3 +106,7 @@ output "ecs_cluster_arn" { output "ecs_cluster_name" { value = aws_ecs_cluster.forms.name } + +output "ecs_events_role_arn" { + value = aws_iam_role.ecs_events.arn +} From ba8c73da8361725f51981e29557eecc78427bac9 Mon Sep 17 00:00:00 2001 From: Sean Rankine Date: Fri, 20 Feb 2026 13:12:46 +0000 Subject: [PATCH 2/3] Add ECS Scheduled Task Module Introduce a new module for ECS scheduled tasks, including task definition, CloudWatch event rule, and event target configuration. This makes it easy for us to define multiple ECS scheduled tasks. --- infra/modules/ecs-scheduled-task/main.tf | 64 +++++++++++++++ infra/modules/ecs-scheduled-task/outputs.tf | 3 + infra/modules/ecs-scheduled-task/variables.tf | 80 +++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 infra/modules/ecs-scheduled-task/main.tf create mode 100644 infra/modules/ecs-scheduled-task/outputs.tf create mode 100644 infra/modules/ecs-scheduled-task/variables.tf diff --git a/infra/modules/ecs-scheduled-task/main.tf b/infra/modules/ecs-scheduled-task/main.tf new file mode 100644 index 000000000..b5836853d --- /dev/null +++ b/infra/modules/ecs-scheduled-task/main.tf @@ -0,0 +1,64 @@ +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +resource "aws_ecs_task_definition" "this" { + family = var.task_name + + execution_role_arn = var.execution_role_arn + task_role_arn = var.task_role_arn + requires_compatibilities = var.requires_compatibilities + cpu = var.cpu + memory = var.memory + network_mode = "awsvpc" + track_latest = true + + runtime_platform { + operating_system_family = "LINUX" + cpu_architecture = "ARM64" + } + + container_definitions = jsonencode([merge( + var.base_task_container_definition, + { + name = "main" + command = var.command + logConfiguration = { + logDriver = "awslogs", + options = { + awslogs-group = var.application_log_group_name, + awslogs-region = data.aws_region.current.name, + awslogs-stream-prefix = var.task_name + } + } + } + )]) +} + +resource "aws_cloudwatch_event_rule" "this" { + name = var.task_name + description = "Trigger the ${var.task_name} ECS task on a schedule" + schedule_expression = var.schedule_expression +} + +resource "aws_cloudwatch_event_target" "this" { + arn = var.ecs_cluster_arn + rule = aws_cloudwatch_event_rule.this.name + role_arn = var.scheduler_role_arn + + ecs_target { + # EventBridge must target task family ARN without revision to always run latest. + task_definition_arn = "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:task-definition/${aws_ecs_task_definition.this.family}" + launch_type = "FARGATE" + platform_version = var.platform_version + + network_configuration { + assign_public_ip = false + security_groups = var.network_security_groups + subnets = var.network_subnets + } + } + + dead_letter_config { + arn = var.eventbridge_dead_letter_queue_arn + } +} diff --git a/infra/modules/ecs-scheduled-task/outputs.tf b/infra/modules/ecs-scheduled-task/outputs.tf new file mode 100644 index 000000000..4be4d1df8 --- /dev/null +++ b/infra/modules/ecs-scheduled-task/outputs.tf @@ -0,0 +1,3 @@ +output "event_rule_name" { + value = aws_cloudwatch_event_rule.this.name +} diff --git a/infra/modules/ecs-scheduled-task/variables.tf b/infra/modules/ecs-scheduled-task/variables.tf new file mode 100644 index 000000000..08bea3a08 --- /dev/null +++ b/infra/modules/ecs-scheduled-task/variables.tf @@ -0,0 +1,80 @@ +variable "task_name" { + type = string + description = "The scheduled task name." +} + +variable "schedule_expression" { + type = string + description = "EventBridge schedule expression, for example cron(...) or rate(...)." +} + +variable "command" { + type = list(string) + description = "Container command override for the scheduled task." +} + +variable "ecs_cluster_arn" { + type = string + description = "ECS cluster ARN targeted by EventBridge." +} + +variable "scheduler_role_arn" { + type = string + description = "Shared IAM role ARN used by EventBridge to run ECS scheduled tasks." +} + +variable "eventbridge_dead_letter_queue_arn" { + type = string + description = "EventBridge dead-letter queue ARN." +} + +variable "base_task_container_definition" { + type = any + description = "Base container definition to clone from the app ECS service." +} + +variable "application_log_group_name" { + type = string + description = "CloudWatch Logs group name used by the task container." +} + +variable "execution_role_arn" { + type = string + description = "Execution role ARN for the ECS task definition." +} + +variable "task_role_arn" { + type = string + description = "Task role ARN for the ECS task definition." +} + +variable "requires_compatibilities" { + type = list(string) + description = "Task definition launch compatibilities." +} + +variable "cpu" { + type = any + description = "Task definition CPU value." +} + +variable "memory" { + type = any + description = "Task definition memory value." +} + +variable "network_security_groups" { + type = list(string) + description = "Security groups for the scheduled ECS task network config." +} + +variable "network_subnets" { + type = list(string) + description = "Subnets for the scheduled ECS task network config." +} + +variable "platform_version" { + type = string + description = "ECS Fargate platform version." + default = "1.4.0" +} From 150486afa1c69ab671fd6a4062f25de9d52fb91b Mon Sep 17 00:00:00 2001 From: Sean Rankine Date: Fri, 20 Feb 2026 13:31:19 +0000 Subject: [PATCH 3/3] Add ECS schedule tasks module for forms admin This provides a place to easily define the schedule tasks. --- infra/deployments/forms/forms-admin/main.tf | 1 + infra/modules/forms-admin/scheduled-tasks.tf | 27 ++++++++++++++++++++ infra/modules/forms-admin/variables.tf | 5 ++++ 3 files changed, 33 insertions(+) create mode 100644 infra/modules/forms-admin/scheduled-tasks.tf diff --git a/infra/deployments/forms/forms-admin/main.tf b/infra/deployments/forms/forms-admin/main.tf index 27abfdee6..ae7887537 100644 --- a/infra/deployments/forms/forms-admin/main.tf +++ b/infra/deployments/forms/forms-admin/main.tf @@ -34,6 +34,7 @@ module "forms_admin" { eventbridge_dead_letter_queue_arn = data.terraform_remote_state.forms_environment.outputs.eventbridge_dead_letter_queue_arn zendesk_sns_topic_arn = data.terraform_remote_state.forms_environment.outputs.zendesk_alert_eu_west_2_topic_arn ecs_cluster_arn = data.terraform_remote_state.forms_environment.outputs.ecs_cluster_arn + ecs_events_role_arn = data.terraform_remote_state.forms_environment.outputs.ecs_events_role_arn alb_arn_suffix = data.terraform_remote_state.forms_environment.outputs.alb_arn_suffix alb_listener_arn = data.terraform_remote_state.forms_environment.outputs.alb_main_listener_arn internal_alb_listener_arn = data.terraform_remote_state.forms_environment.outputs.internal_alb_listener_arn diff --git a/infra/modules/forms-admin/scheduled-tasks.tf b/infra/modules/forms-admin/scheduled-tasks.tf new file mode 100644 index 000000000..4d8050837 --- /dev/null +++ b/infra/modules/forms-admin/scheduled-tasks.tf @@ -0,0 +1,27 @@ +locals { + scheduled_tasks = {} +} + +module "scheduled_tasks" { + for_each = { + for task_name, task in local.scheduled_tasks : task_name => task + if task.enabled + } + source = "../ecs-scheduled-task" + + task_name = "forms-admin-${replace(each.key, "_", "-")}" + schedule_expression = each.value.schedule_expression + command = each.value.command + ecs_cluster_arn = var.ecs_cluster_arn + scheduler_role_arn = var.ecs_events_role_arn + eventbridge_dead_letter_queue_arn = var.eventbridge_dead_letter_queue_arn + base_task_container_definition = module.ecs_service.task_container_definition + application_log_group_name = module.ecs_service.application_log_group_name + execution_role_arn = module.ecs_service.task_definition.execution_role_arn + task_role_arn = module.ecs_service.task_definition.task_role_arn + requires_compatibilities = module.ecs_service.task_definition.requires_compatibilities + cpu = module.ecs_service.task_definition.cpu + memory = module.ecs_service.task_definition.memory + network_security_groups = module.ecs_service.service.network_configuration[0].security_groups + network_subnets = module.ecs_service.service.network_configuration[0].subnets +} diff --git a/infra/modules/forms-admin/variables.tf b/infra/modules/forms-admin/variables.tf index d13c4ff6d..e3b909596 100644 --- a/infra/modules/forms-admin/variables.tf +++ b/infra/modules/forms-admin/variables.tf @@ -143,6 +143,11 @@ variable "ecs_cluster_arn" { description = "The arn for the ECS cluster" } +variable "ecs_events_role_arn" { + type = string + description = "The arn for the shared ECS Events role used by scheduled tasks" +} + variable "alb_arn_suffix" { type = string description = "The suffix of the Application Load Balancer ARN. Used with CloudWatch metrics"