Repository: companieshouse/penalty-payment-api
Pull request #113: SAN-346: ECS Migration
Author: login: dnair-ch
Created: 2025-05-16T08:22:12Z | Merged: 2025-05-16T15:37:44Z



Diff:
diff --git a/README.md b/README.md
index 88fd8d49..a9606cc8 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@ The only external finance system currently supported is E5.
 
 ## Docker support
 
-Pull image from private CH registry by running `docker pull 169942020521.dkr.ecr.eu-west-2.amazonaws.com/local/penalty-payment-api:latest` command or run the following steps to build image locally:
+Pull image from ch-shared-services registry by running `docker pull 416670754337.dkr.ecr.eu-west-2.amazonaws.com/penalty-payment-api:latest` command or run the following steps to build image locally:
 
 1. `export SSH_PRIVATE_KEY_PASSPHRASE='[your SSH key passhprase goes here]'` (optional, set only if SSH key is passphrase protected)
-2. `DOCKER_BUILDKIT=0 docker build --build-arg SSH_PRIVATE_KEY="$(cat ~/.ssh/id_rsa)" --build-arg SSH_PRIVATE_KEY_PASSPHRASE -t 169942020521.dkr.ecr.eu-west-2.amazonaws.com/local/penalty-payment-api:latest .`
+2. `DOCKER_BUILDKIT=0 docker build --build-arg SSH_PRIVATE_KEY="$(cat ~/.ssh/id_rsa)" --build-arg SSH_PRIVATE_KEY_PASSPHRASE -t 416670754337.dkr.ecr.eu-west-2.amazonaws.com/penalty-payment-api:latest .`
diff --git a/ecs-image-build/Dockerfile b/ecs-image-build/Dockerfile
new file mode 100644
index 00000000..b99fa2f3
--- /dev/null
+++ b/ecs-image-build/Dockerfile
@@ -0,0 +1,9 @@
+ARG IMAGE_VERSION="latest"
+FROM 416670754337.dkr.ecr.eu-west-2.amazonaws.com/ci-golang-build-1.24:${IMAGE_VERSION} 
+
+WORKDIR /opt
+
+COPY /app .
+COPY docker_start.sh .
+
+CMD ["./docker_start.sh"]
\ No newline at end of file
diff --git a/ecs-image-build/docker_start.sh b/ecs-image-build/docker_start.sh
new file mode 100644
index 00000000..ffba22c4
--- /dev/null
+++ b/ecs-image-build/docker_start.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+#
+# Start script for penalty-payment-api
+PORT="8080"
+if [[ ! -x ./penalty-payment-api ]]; then
+  echo "ERROR: ./penalty-payment-api not found or not executable"
+  exit 1
+fi
+exec ./penalty-payment-api "-bind-addr=:${PORT}"
\ No newline at end of file
diff --git a/ecs-image-build/version b/ecs-image-build/version
new file mode 100644
index 00000000..a1ec18c9
--- /dev/null
+++ b/ecs-image-build/version
@@ -0,0 +1 @@
+ecs-1.0
\ No newline at end of file
diff --git a/terraform/groups/ecs-service/data.tf b/terraform/groups/ecs-service/data.tf
new file mode 100644
index 00000000..0dd9d61a
--- /dev/null
+++ b/terraform/groups/ecs-service/data.tf
@@ -0,0 +1,79 @@
+data "vault_generic_secret" "stack_secrets" {
+  path = "applications/${var.aws_profile}/${var.environment}/${local.stack_name}-stack"
+}
+
+data "aws_kms_key" "kms_key" {
+  key_id = local.kms_alias
+}
+
+data "vault_generic_secret" "service_secrets" {
+  path = "applications/${var.aws_profile}/${var.environment}/${local.stack_name}-stack/${local.service_name}"
+}
+
+data "aws_vpc" "vpc" {
+  filter {
+    name   = "tag:Name"
+    values = [local.vpc_name]
+  }
+}
+
+# Get application subnet IDs
+data "aws_subnets" "application" {
+  filter {
+    name   = "tag:Name"
+    values = [local.application_subnet_pattern]
+  }
+}
+
+data "aws_ecs_cluster" "ecs_cluster" {
+  cluster_name = "${local.name_prefix}-cluster"
+}
+
+data "aws_iam_role" "ecs_cluster_iam_role" {
+  name = "${local.name_prefix}-ecs-task-execution-role"
+}
+
+data "aws_lb" "service_lb" {
+  name = "${var.environment}-chs-apichgovuk"
+}
+
+data "aws_lb_listener" "service_lb_listener" {
+  load_balancer_arn = data.aws_lb.service_lb.arn
+  port              = 443
+}
+
+data "aws_lb" "secondary_lb" {
+  name = "${var.environment}-chs-apichgovuk-private"
+}
+
+data "aws_lb_listener" "secondary_lb_listener" {
+  load_balancer_arn = data.aws_lb.secondary_lb.arn
+  port = 443
+}
+
+# retrieve all secrets for this stack using the stack path
+data "aws_ssm_parameters_by_path" "secrets" {
+  path = "/${local.name_prefix}"
+}
+
+# create a list of secrets names to retrieve them in a nicer format and lookup each secret by name
+data "aws_ssm_parameter" "secret" {
+  for_each = toset(data.aws_ssm_parameters_by_path.secrets.names)
+  name = each.key
+}
+
+# retrieve all global secrets for this env using global path
+data "aws_ssm_parameters_by_path" "global_secrets" {
+  path = "/${local.global_prefix}"
+}
+
+# create a list of secrets names to retrieve them in a nicer format and lookup each secret by name
+data "aws_ssm_parameter" "global_secret" {
+  for_each = toset(data.aws_ssm_parameters_by_path.global_secrets.names)
+  name     = each.key
+}
+
+// --- s3 bucket for shared services config ---
+data "vault_generic_secret" "shared_s3" {
+  path = "aws-accounts/shared-services/s3"
+}
diff --git a/terraform/groups/ecs-service/locals.tf b/terraform/groups/ecs-service/locals.tf
new file mode 100644
index 00000000..370e3777
--- /dev/null
+++ b/terraform/groups/ecs-service/locals.tf
@@ -0,0 +1,74 @@
+# Define all hardcoded local variable and local variables looked up from data resources 
+
+locals {
+  stack_name                 = "company-requests" # this must match the stack name the service deploys into
+  name_prefix                = "${local.stack_name}-${var.environment}"
+  global_prefix              = "global-${var.environment}"
+  service_name               = "penalty-payment-api"
+  container_port             = "8080"
+  eric_port                  = "10000"
+  docker_repo                = "penalty-payment-api"
+  kms_alias                  = "alias/${var.aws_profile}/environment-services-kms"
+  lb_listener_rule_priority  = 200
+  lb_listener_paths          = ["/company/*/penalties/*"]
+  healthcheck_path           = "/penalty-payment-api/healthcheck" # healthcheck path for penalty-payment-api
+  healthcheck_matcher        = "200"
+  vpc_name                   = local.stack_secrets["vpc_name"]
+  s3_config_bucket           = data.vault_generic_secret.shared_s3.data["config_bucket_name"]
+  app_environment_filename   = "penalty-payment-api.env"
+  use_set_environment_files  = var.use_set_environment_files
+  application_subnet_ids     = data.aws_subnets.application.ids
+  application_subnet_pattern = local.stack_secrets["application_subnet_pattern"]
+
+  stack_secrets   = jsondecode(data.vault_generic_secret.stack_secrets.data_json)
+  service_secrets = jsondecode(data.vault_generic_secret.service_secrets.data_json)
+
+  # create a map of secret name => secret arn to pass into ecs service module
+  # using the trimprefix function to remove the prefixed path from the secret name
+  secrets_arn_map = {
+    for sec in data.aws_ssm_parameter.secret :
+    trimprefix(sec.name, "/${local.name_prefix}/") => sec.arn
+  }
+
+  global_secrets_arn_map = {
+    for sec in data.aws_ssm_parameter.global_secret :
+    trimprefix(sec.name, "/${local.global_prefix}/") => sec.arn
+  }
+
+  global_secret_list = flatten([for key, value in local.global_secrets_arn_map :
+    { "name" = upper(key), "valueFrom" = value }
+  ])
+
+  ssm_global_version_map = [
+    for sec in data.aws_ssm_parameter.global_secret :
+      { "name"  = "GLOBAL_${var.ssm_version_prefix}${replace(upper(basename(sec.name)), "-", "_")}", "value" = sec.version }
+  ]
+
+  service_secrets_arn_map = {
+    for sec in module.secrets.secrets :
+    trimprefix(sec.name, "/${local.service_name}-${var.environment}/") => sec.arn
+  }
+
+  service_secret_list = flatten([for key, value in local.service_secrets_arn_map :
+    { "name" = upper(key), "valueFrom" = value }
+  ])
+
+  ssm_service_version_map = [
+    for sec in module.secrets.secrets :
+      { "name"  = "${replace(upper(local.service_name), "-", "_")}_${var.ssm_version_prefix}${replace(upper(basename(sec.name)), "-", "_")}", "value" = sec.version }
+  ]
+
+  # secrets to go in list
+  task_secrets = concat(local.global_secret_list,local.service_secret_list)
+
+  task_environment = concat(local.ssm_global_version_map,local.ssm_service_version_map,[
+    { "name" : "PORT", "value" : local.container_port }
+  ])
+
+  # get eric secrets from global secrets map
+  eric_secrets = [
+    { "name": "API_KEY", "valueFrom": local.global_secrets_arn_map.eric_api_key },
+    { "name": "AES256_KEY", "valueFrom": local.global_secrets_arn_map.eric_aes256_key }
+  ]
+  eric_environment_filename = "eric.env"
+}
diff --git a/terraform/groups/ecs-service/main.tf b/terraform/groups/ecs-service/main.tf
new file mode 100644
index 00000000..48f96738
--- /dev/null
+++ b/terraform/groups/ecs-service/main.tf
@@ -0,0 +1,103 @@
+provider "aws" {
+  region = var.aws_region
+}
+
+terraform {
+  backend "s3" {
+  }
+  required_version = "~> 1.3"
+  required_providers {
+    aws = {
+      source  = "hashicorp/aws"
+      version = "~> 4.54.0"
+    }
+    vault = {
+      source  = "hashicorp/vault"
+      version = "~> 3.18.0"
+    }
+  }
+}
+
+module "secrets" {
+  source = "git@github.com:companieshouse/terraform-modules//aws/ecs/secrets?ref=1.0.293"
+
+  name_prefix = "${local.service_name}-${var.environment}"
+  environment = var.environment
+  kms_key_id  = data.aws_kms_key.kms_key.id
+  secrets     = nonsensitive(local.service_secrets)
+}
+
+module "ecs-service" {
+  source = "git@github.com:companieshouse/terraform-modules//aws/ecs/ecs-service?ref=1.0.293"
+
+  # Environmental configuration
+  environment             = var.environment
+  aws_region              = var.aws_region
+  aws_profile             = var.aws_profile
+  vpc_id                  = data.aws_vpc.vpc.id
+  ecs_cluster_id          = data.aws_ecs_cluster.ecs_cluster.id
+  task_execution_role_arn = data.aws_iam_role.ecs_cluster_iam_role.arn
+
+  # Load balancer configuration
+  lb_listener_arn                   = data.aws_lb_listener.service_lb_listener.arn
+  lb_listener_rule_priority         = local.lb_listener_rule_priority
+  lb_listener_paths                 = local.lb_listener_paths
+  multilb_setup                     = true
+  multilb_listeners                 = {
+    "priv-api-lb": {
+      listener_arn                  = data.aws_lb_listener.secondary_lb_listener.arn,
+      load_balancer_arn             = data.aws_lb.secondary_lb.arn
+    }
+    "pub-api-lb": {
+      load_balancer_arn      = data.aws_lb.service_lb.arn
+      listener_arn           = data.aws_lb_listener.service_lb_listener.arn
+    }
+  }
+
+  # ECS Task container health check
+  use_task_container_healthcheck = true
+  healthcheck_path               = local.healthcheck_path
+  healthcheck_matcher            = local.healthcheck_matcher
+  health_check_grace_period_seconds = 300
+  healthcheck_healthy_threshold     = "2"
+
+  # Docker container details
+  docker_registry   = var.docker_registry
+  docker_repo       = local.docker_repo
+  container_version = var.penalty_payment_api_version
+  container_port    = local.container_port
+
+  # Service configuration
+  service_name                       = local.service_name
+  name_prefix                        = local.name_prefix
+  desired_task_count                 = var.desired_task_count
+  max_task_count                     = var.max_task_count
+  required_cpus                      = var.required_cpus
+  required_memory                    = var.required_memory
+  service_autoscale_enabled          = var.service_autoscale_enabled
+  service_autoscale_target_value_cpu = var.service_autoscale_target_value_cpu
+  service_scaledown_schedule         = var.service_scaledown_schedule
+  service_scaleup_schedule           = var.service_scaleup_schedule
+  use_capacity_provider              = var.use_capacity_provider
+  use_fargate                        = var.use_fargate
+  fargate_subnets                    = local.application_subnet_ids
+
+  # Cloudwatch
+  cloudwatch_alarms_enabled = var.cloudwatch_alarms_enabled
+
+  # Service environment variable and secret configs
+  task_environment          = local.task_environment
+  task_secrets              = local.task_secrets
+  app_environment_filename  = local.app_environment_filename
+  use_set_environment_files = local.use_set_environment_files
+  read_only_root_filesystem = false
+
+  # eric options for eric running API module
+  use_eric_reverse_proxy    = true
+  eric_version              = var.eric_version
+  eric_cpus                 = var.eric_cpus
+  eric_memory               = var.eric_memory
+  eric_port                 = local.eric_port
+  eric_environment_filename = local.eric_environment_filename
+  eric_secrets              = local.eric_secrets
+}
diff --git a/terraform/groups/ecs-service/profiles/development-eu-west-2/cidev/vars b/terraform/groups/ecs-service/profiles/development-eu-west-2/cidev/vars
new file mode 100644
index 00000000..32b4c25d
--- /dev/null
+++ b/terraform/groups/ecs-service/profiles/development-eu-west-2/cidev/vars
@@ -0,0 +1,5 @@
+environment = "cidev"
+aws_profile = "development-eu-west-2"
+
+# service configs
+use_set_environment_files = true
\ No newline at end of file
diff --git a/terraform/groups/ecs-service/profiles/live-eu-west-2/live/vars b/terraform/groups/ecs-service/profiles/live-eu-west-2/live/vars
new file mode 100644
index 00000000..0d1ffae8
--- /dev/null
+++ b/terraform/groups/ecs-service/profiles/live-eu-west-2/live/vars
@@ -0,0 +1,5 @@
+environment = "live"
+aws_profile = "live-eu-west-2"
+
+# service configs
+use_set_environment_files = true
diff --git a/terraform/groups/ecs-service/profiles/staging-eu-west-2/staging/vars b/terraform/groups/ecs-service/profiles/staging-eu-west-2/staging/vars
new file mode 100644
index 00000000..370967d1
--- /dev/null
+++ b/terraform/groups/ecs-service/profiles/staging-eu-west-2/staging/vars
@@ -0,0 +1,10 @@
+environment = "staging"
+aws_profile = "staging-eu-west-2"
+
+# service configs
+use_set_environment_files = true
+
+# scaling configs
+service_autoscale_enabled  = true
+service_scaledown_schedule = "55 19 * * ? *"
+service_scaleup_schedule   = "5 6 * * ? *"
\ No newline at end of file
diff --git a/terraform/groups/ecs-service/variables.tf b/terraform/groups/ecs-service/variables.tf
new file mode 100644
index 00000000..b839d889
--- /dev/null
+++ b/terraform/groups/ecs-service/variables.tf
@@ -0,0 +1,129 @@
+# ------------------------------------------------------------------------------
+# Environment
+# ------------------------------------------------------------------------------
+variable "environment" {
+  type        = string
+  description = "The environment name, defined in envrionment vars."
+}
+variable "aws_region" {
+  default     = "eu-west-2"
+  type        = string
+  description = "The AWS region for deployment."
+}
+variable "aws_profile" {
+  default     = "development-eu-west-2"
+  type        = string
+  description = "The AWS profile to use for deployment."
+}
+
+# ------------------------------------------------------------------------------
+# Docker Container
+# ------------------------------------------------------------------------------
+variable "docker_registry" {
+  type        = string
+  description = "The FQDN of the Docker registry."
+}
+
+# ------------------------------------------------------------------------------
+# Service performance and scaling configs
+# ------------------------------------------------------------------------------
+variable "desired_task_count" {
+  type        = number
+  description = "The desired ECS task count for this service"
+  default     = 1 # defaulted low for dev environments, override for production if required
+}
+variable "max_task_count" {
+  type        = number
+  description = "The maximum number of tasks for this service."
+  default     = 3
+}
+variable "required_cpus" {
+  type        = number
+  description = "The required cpu resource for this service. 1024 here is 1 vCPU"
+  default     = 256 # defaulted minimum required for fargate services, override for production
+}
+variable "required_memory" {
+  type        = number
+  description = "The required memory for this service"
+  default     = 512 # defaulted minimum required for fargate services, override for production
+}
+variable "eric_cpus" {
+  type = number
+  description = "The required cpu resource for eric. 1024 here is 1 vCPU"
+  default = 256
+}
+variable "eric_memory" {
+  type = number
+  description = "The required memory for eric"
+  default = 512
+}
+
+variable "use_fargate" {
+  type        = bool
+  description = "If true, sets the required capabilities for all containers in the task definition to use FARGATE, false uses EC2"
+  default     = true
+}
+
+variable "use_capacity_provider" {
+  type        = bool
+  description = "Whether to use a capacity provider instead of setting a launch type for the service"
+  default     = true
+}
+
+variable "service_autoscale_enabled" {
+  type        = bool
+  description = "Whether to enable service autoscaling, including scheduled autoscaling"
+  default     = true
+}
+
+variable "service_autoscale_target_value_cpu" {
+  type        = number
+  description = "Target CPU percentage for the ECS Service to autoscale on"
+  default     = 50 # 100 disables autoscaling using CPU as a metric
+}
+
+variable "service_scaledown_schedule" {
+  type        = string
+  description = "The schedule to use when scaling down the number of tasks to zero."
+  default     = ""
+}
+
+variable "service_scaleup_schedule" {
+  type        = string
+  description = "The schedule to use when scaling up the number of tasks to their normal desired level."
+  default     = ""
+}
+
+# ----------------------------------------------------------------------
+# Cloudwatch alerts
+# ----------------------------------------------------------------------
+variable "cloudwatch_alarms_enabled" {
+  description = "Whether to create a standard set of cloudwatch alarms for the service.  Requires an SNS topic to have already been created for the stack."
+  type        = bool
+  default     = true
+}
+
+# ------------------------------------------------------------------------------
+# Service environment variable configs
+# ------------------------------------------------------------------------------
+variable "ssm_version_prefix" {
+  type        = string
+  description = "String to use as a prefix to the names of the variables containing variables and secrets version."
+  default     = "SSM_VERSION_"
+}
+
+variable "use_set_environment_files" {
+  type        = bool
+  default     = false
+  description = "Toggle default global and shared  environment files"
+}
+
+variable "penalty_payment_api_version" {
+  type        = string
+  description = "The version of the penalty_payment_api container to run."
+}
+
+variable "eric_version" {
+  type        = string
+  description = "The version of the eric container to run."
+}
diff --git a/terraform/groups/ecs-service/vault-providers/approle b/terraform/groups/ecs-service/vault-providers/approle
new file mode 100644
index 00000000..4d73f37f
--- /dev/null
+++ b/terraform/groups/ecs-service/vault-providers/approle
@@ -0,0 +1,20 @@
+variable "hashicorp_vault_role_id" {
+  description = "The role identifier used when retrieving configuration from Hashicorp Vault"
+  type = string
+}
+
+variable "hashicorp_vault_secret_id" {
+  description = "The secret identifier used when retrieving configuration from Hashicorp Vault"
+  type = string
+}
+
+provider "vault" {
+  auth_login {
+    path = "auth/approle/login"
+
+    parameters = {
+      role_id   = var.hashicorp_vault_role_id
+      secret_id = var.hashicorp_vault_secret_id
+    }
+  }
+}
\ No newline at end of file
diff --git a/terraform/groups/ecs-service/vault-providers/token b/terraform/groups/ecs-service/vault-providers/token
new file mode 100644
index 00000000..28cb7213
--- /dev/null
+++ b/terraform/groups/ecs-service/vault-providers/token
@@ -0,0 +1,5 @@
+provider "vault" {
+    # Credentials read from the environment variables:
+    #   ${VAULT_ADDR}
+    #   ${VAULT_TOKEN}
+}
\ No newline at end of file
diff --git a/terraform/groups/ecs-service/vault-providers/userpass b/terraform/groups/ecs-service/vault-providers/userpass
new file mode 100644
index 00000000..efea25a8
--- /dev/null
+++ b/terraform/groups/ecs-service/vault-providers/userpass
@@ -0,0 +1,19 @@
+variable "hashicorp_vault_username" {
+  description = "The username used when retrieving configuration from Hashicorp Vault"
+  type        = string
+}
+
+variable "hashicorp_vault_password" {
+  description = "The password used when retrieving configuration from Hashicorp Vault"
+  type        = string
+}
+
+provider "vault" {
+  auth_login {
+    path = "auth/userpass/login/${var.hashicorp_vault_username}"
+
+    parameters = {
+      password = var.hashicorp_vault_password
+    }
+  }
+}
\ No newline at end of file
diff --git a/terraform/groups/ecs-service/vault.tf b/terraform/groups/ecs-service/vault.tf
new file mode 100644
index 00000000..f3075ece
--- /dev/null
+++ b/terraform/groups/ecs-service/vault.tf
@@ -0,0 +1,19 @@
+variable "hashicorp_vault_username" {
+  description = "The username used when retrieving configuration from Hashicorp Vault"
+  type        = string
+}
+
+variable "hashicorp_vault_password" {
+  description = "The password used when retrieving configuration from Hashicorp Vault"
+  type        = string
+}
+
+provider "vault" {
+  auth_login {
+    path = "auth/userpass/login/${var.hashicorp_vault_username}"
+
+    parameters = {
+      password = var.hashicorp_vault_password
+    }
+  }
+}
diff --git a/terraform/groups/ecs-service/version b/terraform/groups/ecs-service/version
new file mode 100644
index 00000000..4dba8566
--- /dev/null
+++ b/terraform/groups/ecs-service/version
@@ -0,0 +1 @@
+ecs-service-1.0
\ No newline at end of file


Reviews:
user: login: dgordon-ch
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:29:57Z
user: login: acallaghan-ch
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:31:42Z
user: login: rbull-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:53:55Z
user: login: dnair-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:56:51Z
user: login: rbull-ch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:00:43Z
user: login: rbull-ch
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:34:49Z

Review comments:
diff_hunk: @@ -0,0 +1,9 @@
+ARG IMAGE_VERSION="latest"
+FROM 416670754337.dkr.ecr.eu-west-2.amazonaws.com/ci-golang-build-1.24:${IMAGE_VERSION} 
path: ecs-image-build/Dockerfile
user: login: rbull-ch
body: Should this be `:latest` like these [ci-golang-build-1.24:latest](https://github.com/search?q=org%3Acompanieshouse%20FROM%20416670754337.dkr.ecr.eu-west-2.amazonaws.com%2Fci-golang-build-1.24&type=code)?
created_at: 2025-05-16T10:53:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,9 @@
+ARG IMAGE_VERSION="latest"
+FROM 416670754337.dkr.ecr.eu-west-2.amazonaws.com/ci-golang-build-1.24:${IMAGE_VERSION} 
path: ecs-image-build/Dockerfile
user: login: dnair-ch
body: > Should this be `:latest` like these [ci-golang-build-1.24:latest](https://github.com/search?q=org%3Acompanieshouse%20FROM%20416670754337.dkr.ecr.eu-west-2.amazonaws.com%2Fci-golang-build-1.24&type=code)?

ARG IMAGE_VERSION="latest" This will resolve that during runtime

created_at: 2025-05-16T10:56:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,9 @@
+ARG IMAGE_VERSION="latest"
+FROM 416670754337.dkr.ecr.eu-west-2.amazonaws.com/ci-golang-build-1.24:${IMAGE_VERSION} 
path: ecs-image-build/Dockerfile
user: login: rbull-ch
body: Although that doesn't look to me, that will actually fix the [Quality Gate failed](https://github.com/companieshouse/penalty-payment-api/pull/113#issuecomment-2886062772) issues above?
created_at: 2025-05-16T11:00:43Z
author_association: CONTRIBUTOR

PR comments:
user: login: chsonarqubeprchecks[bot]
created_at: 2025-05-16T11:36:19Z
body: ## [![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarqube.companieshouse.gov.uk/dashboard?id=uk.gov.companieshouse%3Apenalty-payment-api&pullRequest=113) **Quality Gate passed**  
Issues  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Apenalty-payment-api&pullRequest=113&resolved=false) [0 New issues](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Apenalty-payment-api&pullRequest=113&resolved=false)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/fixed-16px.png '')](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Apenalty-payment-api&fixedInPullRequest=113) [0 Fixed issues](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Apenalty-payment-api&fixedInPullRequest=113)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Apenalty-payment-api&pullRequest=113&issueStatuses=ACCEPTED) [1 Accepted issue](https://sonarqube.companieshouse.gov.uk/project/issues?id=uk.gov.companieshouse%3Apenalty-payment-api&pullRequest=113&issueStatuses=ACCEPTED)

Measures  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.companieshouse.gov.uk/security_hotspots?id=uk.gov.companieshouse%3Apenalty-payment-api&pullRequest=113) [0 Security Hotspots](https://sonarqube.companieshouse.gov.uk/security_hotspots?id=uk.gov.companieshouse%3Apenalty-payment-api&pullRequest=113)  
![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/no-data-16px.png '') No data about Coverage  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonarqube.companieshouse.gov.uk/component_measures?id=uk.gov.companieshouse%3Apenalty-payment-api&pullRequest=113&metric=new_duplicated_lines_density&view=list) [0.0% Duplication on New Code](https://sonarqube.companieshouse.gov.uk/component_measures?id=uk.gov.companieshouse%3Apenalty-payment-api&pullRequest=113&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube](https://sonarqube.companieshouse.gov.uk/dashboard?id=uk.gov.companieshouse%3Apenalty-payment-api&pullRequest=113)


author_association: NONE