Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions infra/deployments/forms/forms-admin/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions infra/modules/ecs-scheduled-task/main.tf
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 3 additions & 0 deletions infra/modules/ecs-scheduled-task/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
output "event_rule_name" {
value = aws_cloudwatch_event_rule.this.name
}
80 changes: 80 additions & 0 deletions infra/modules/ecs-scheduled-task/variables.tf
Original file line number Diff line number Diff line change
@@ -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"
}
26 changes: 26 additions & 0 deletions infra/modules/environment/ecs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions infra/modules/environment/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
27 changes: 27 additions & 0 deletions infra/modules/forms-admin/scheduled-tasks.tf
Original file line number Diff line number Diff line change
@@ -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, "_", "-")}"
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task name has multiple critical issues:

  1. Pipeline Breakage: The task family name has changed from ${var.environment_name}_forms-admin_mailchimp_sync (e.g., production_forms-admin_mailchimp_sync) to forms-admin-mailchimp-sync. This breaks the deployment pipeline (deploy-forms-admin-container.tf lines 241 and 270) which expects the old naming pattern with underscores and environment prefix. The pipeline's update task definition steps will fail to find the correct tasks.

  2. Logging Issues: The task name is used as the CloudWatch log stream prefix (see ecs-scheduled-task/main.tf line 30). Without the environment name, logs from all environments will have the same prefix (forms-admin-mailchimp-sync/main/*), making it impossible to filter by environment. The old code used forms-admin-${var.env_name}-organisations-sync which included the environment. The troubleshooting documentation in orgs-sync.tf line 43 expects environment-specific log streams.

The task_name should be ${var.env_name}_forms-admin-${replace(each.key, "_", "-")} to match the old pattern and preserve both pipeline compatibility and environment-specific logging.

Suggested change
task_name = "forms-admin-${replace(each.key, "_", "-")}"
task_name = "${var.env_name}_forms-admin-${replace(each.key, "_", "-")}"

Copilot uses AI. Check for mistakes.
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
}
5 changes: 5 additions & 0 deletions infra/modules/forms-admin/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading