From 3b0c0c16eb3fa174cd2a08113fd7b07d6d924fec Mon Sep 17 00:00:00 2001 From: oliverbeumkes-nhs Date: Thu, 26 Feb 2026 15:05:35 +0000 Subject: [PATCH 01/12] [PRM-726] Reverted end-of-transfer to a point it will run against itself on a destroy --- .../suspension-service/terraform/dev.tfvars | 69 ++++++++++++++++++- stacks/suspension-service/terraform/iam.tf | 4 +- stacks/suspension-service/terraform/main.tf | 11 +++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/stacks/suspension-service/terraform/dev.tfvars b/stacks/suspension-service/terraform/dev.tfvars index b5968e04..8f51384c 100644 --- a/stacks/suspension-service/terraform/dev.tfvars +++ b/stacks/suspension-service/terraform/dev.tfvars @@ -5,4 +5,71 @@ repo_name = "suspension-service" metric_namespace = "SuspensionService" can_update_managing_organisation_to_repo = true repo_process_only_safe_listed_ods_codes = true -process_only_synthetic_patients = false \ No newline at end of file +process_only_synthetic_patients = false +variable "region" { + type = string + default = "eu-west-2" +} + +variable "repo_name" { + type = string +} + +variable "environment" {} + +variable "component_name" {} + +variable "metric_namespace" {} + +variable "task_image_tag" {} + +variable "task_cpu" { + default = 512 +} +variable "task_memory" { + default = 1024 +} + +variable "log_level" { + type = string + default = "debug" +} + +variable "process_only_synthetic_patients" { + default = true +} + +variable "repo_process_only_safe_listed_ods_codes" { + default = true +} + +variable "synthetic_patient_prefix" {} + +variable "scale_up_expression" { + type = string + default = "((HOUR(m1)==17 && MINUTE(m1)==58)),10,0" +} + +variable "enable_scale_action" { + type = bool + default = true +} + +variable "can_update_managing_organisation_to_repo" { + description = "Toggle to allow updating managing organisation to repo ODS code" + default = false +} + +variable "is_end_of_transfer_service" { + type = bool + default = false +} + +variable "image_name" { + type = string + default = "suspension-service" +} + +variable "ecs_desired_count" { + default = 0 +} \ No newline at end of file diff --git a/stacks/suspension-service/terraform/iam.tf b/stacks/suspension-service/terraform/iam.tf index 2344c417..15645dcb 100644 --- a/stacks/suspension-service/terraform/iam.tf +++ b/stacks/suspension-service/terraform/iam.tf @@ -9,9 +9,9 @@ locals { aws_sns_topic.deceased_patient.arn, aws_sns_topic.event_out_of_order.arn, aws_sns_topic.active_suspensions.arn, + module.end-of-transfer-service[0].end_of_transfer_sns_topic ] - sns_for_suspension_service = module.suspension-service.repo_incoming_sns_topic - sns_arns = concat(local.sns_base_arns, [local.sns_for_suspension_service]) + sns_arns = local.sns_base_arns } data "aws_iam_policy_document" "ecs-assume-role-policy" { diff --git a/stacks/suspension-service/terraform/main.tf b/stacks/suspension-service/terraform/main.tf index de1e9d18..9fe969b8 100644 --- a/stacks/suspension-service/terraform/main.tf +++ b/stacks/suspension-service/terraform/main.tf @@ -11,6 +11,17 @@ terraform { } } +module "end-of-transfer-service" { + count = 1 + source = "./modules/end-of-transfer-service/" + environment = var.environment + + component_name = var.component_name + metric_namespace = var.metric_namespace + repo_name = var.repo_name + ecs_desired_count = var.ecs_desired_count +} + module "suspension-service" { source = "./modules/suspension-service/" environment = var.environment From a266cb05a348ef344f8afaf591c2c1f4909ca7bb Mon Sep 17 00:00:00 2001 From: oliverbeumkes-nhs Date: Thu, 26 Feb 2026 15:05:52 +0000 Subject: [PATCH 02/12] [PRM-726] Reverted end-of-transfer to a point it will run against itself on a destroy --- .../end-of-transfer-service/dev.tfvars | 10 + .../end-of-transfer-service.tfvars | 6 + .../end-of-transfer-service/perf.tfvars | 8 + .../end-of-transfer-service/pre-prod.tfvars | 6 + .../end-of-transfer-service/prod.tfvars | 6 + .../end-of-transfer-service/test.tfvars | 8 + .../end-of-transfer-service/cloudwatch.tf | 24 ++ .../modules/end-of-transfer-service/data.tf | 6 + .../modules/end-of-transfer-service/iam.tf | 35 +++ .../modules/end-of-transfer-service/output.tf | 11 + .../modules/end-of-transfer-service/queue.tf | 46 +++ .../end-of-transfer-service/variables.tf | 18 ++ stacks/suspension-service/terraform/tasks | 274 ++++++++++++++++++ 13 files changed, 458 insertions(+) create mode 100644 stacks/suspension-service/terraform/configs/end-of-transfer-service/dev.tfvars create mode 100644 stacks/suspension-service/terraform/configs/end-of-transfer-service/end-of-transfer-service.tfvars create mode 100644 stacks/suspension-service/terraform/configs/end-of-transfer-service/perf.tfvars create mode 100644 stacks/suspension-service/terraform/configs/end-of-transfer-service/pre-prod.tfvars create mode 100644 stacks/suspension-service/terraform/configs/end-of-transfer-service/prod.tfvars create mode 100644 stacks/suspension-service/terraform/configs/end-of-transfer-service/test.tfvars create mode 100644 stacks/suspension-service/terraform/modules/end-of-transfer-service/cloudwatch.tf create mode 100644 stacks/suspension-service/terraform/modules/end-of-transfer-service/data.tf create mode 100644 stacks/suspension-service/terraform/modules/end-of-transfer-service/iam.tf create mode 100644 stacks/suspension-service/terraform/modules/end-of-transfer-service/output.tf create mode 100644 stacks/suspension-service/terraform/modules/end-of-transfer-service/queue.tf create mode 100644 stacks/suspension-service/terraform/modules/end-of-transfer-service/variables.tf create mode 100755 stacks/suspension-service/terraform/tasks diff --git a/stacks/suspension-service/terraform/configs/end-of-transfer-service/dev.tfvars b/stacks/suspension-service/terraform/configs/end-of-transfer-service/dev.tfvars new file mode 100644 index 00000000..68f581cb --- /dev/null +++ b/stacks/suspension-service/terraform/configs/end-of-transfer-service/dev.tfvars @@ -0,0 +1,10 @@ +environment = "dev" + +synthetic_patient_prefix = "96937" +process_only_synthetic_patients = false + +scale_up_expression = "( (MINUTE(m1)>=0 )),10, 0" +enable_scale_action = false + +ecs_desired_count = 0 +repo_process_only_safe_listed_ods_codes = true \ No newline at end of file diff --git a/stacks/suspension-service/terraform/configs/end-of-transfer-service/end-of-transfer-service.tfvars b/stacks/suspension-service/terraform/configs/end-of-transfer-service/end-of-transfer-service.tfvars new file mode 100644 index 00000000..89cd5168 --- /dev/null +++ b/stacks/suspension-service/terraform/configs/end-of-transfer-service/end-of-transfer-service.tfvars @@ -0,0 +1,6 @@ +component_name = "end-of-transfer-service" +repo_name = "end-of-transfer-service" +metric_namespace = "EndOfTransferService" + +can_update_managing_organisation_to_repo = false +is_end_of_transfer_service = true diff --git a/stacks/suspension-service/terraform/configs/end-of-transfer-service/perf.tfvars b/stacks/suspension-service/terraform/configs/end-of-transfer-service/perf.tfvars new file mode 100644 index 00000000..95c15b4b --- /dev/null +++ b/stacks/suspension-service/terraform/configs/end-of-transfer-service/perf.tfvars @@ -0,0 +1,8 @@ +environment = "perf" + +synthetic_patient_prefix = "96936" +process_only_synthetic_patients = false + +log_level = "info" + +ecs_desired_count = 0 \ No newline at end of file diff --git a/stacks/suspension-service/terraform/configs/end-of-transfer-service/pre-prod.tfvars b/stacks/suspension-service/terraform/configs/end-of-transfer-service/pre-prod.tfvars new file mode 100644 index 00000000..ea6addf1 --- /dev/null +++ b/stacks/suspension-service/terraform/configs/end-of-transfer-service/pre-prod.tfvars @@ -0,0 +1,6 @@ +environment = "pre-prod" + +synthetic_patient_prefix = "96936" +process_only_synthetic_patients = true + +ecs_desired_count = 0 \ No newline at end of file diff --git a/stacks/suspension-service/terraform/configs/end-of-transfer-service/prod.tfvars b/stacks/suspension-service/terraform/configs/end-of-transfer-service/prod.tfvars new file mode 100644 index 00000000..fcba9bce --- /dev/null +++ b/stacks/suspension-service/terraform/configs/end-of-transfer-service/prod.tfvars @@ -0,0 +1,6 @@ +environment = "prod" + +synthetic_patient_prefix = "999" +process_only_synthetic_patients = true + +ecs_desired_count = 0 \ No newline at end of file diff --git a/stacks/suspension-service/terraform/configs/end-of-transfer-service/test.tfvars b/stacks/suspension-service/terraform/configs/end-of-transfer-service/test.tfvars new file mode 100644 index 00000000..454adca9 --- /dev/null +++ b/stacks/suspension-service/terraform/configs/end-of-transfer-service/test.tfvars @@ -0,0 +1,8 @@ +environment = "test" + +synthetic_patient_prefix = "96941" +process_only_synthetic_patients = true + +scale_up_expression = "( (MINUTE(m1)==0 || MINUTE(m1)==15 || MINUTE(m1)==30 || MINUTE(m1)==45 )),10, 0" + +ecs_desired_count = 0 \ No newline at end of file diff --git a/stacks/suspension-service/terraform/modules/end-of-transfer-service/cloudwatch.tf b/stacks/suspension-service/terraform/modules/end-of-transfer-service/cloudwatch.tf new file mode 100644 index 00000000..af7edee6 --- /dev/null +++ b/stacks/suspension-service/terraform/modules/end-of-transfer-service/cloudwatch.tf @@ -0,0 +1,24 @@ +locals { + sqs_namespace = "AWS/SQS" +} + +resource "aws_cloudwatch_metric_alarm" "transfer_complete_queue_age_of_message" { + alarm_name = "${var.environment}-${var.component_name}-transfer-complete-approx-age-of-oldest-message" + comparison_operator = "GreaterThanThreshold" + threshold = "1800" + evaluation_periods = "1" + metric_name = "ApproximateAgeOfOldestMessage" + namespace = local.sqs_namespace + alarm_description = "This alarm triggers when messages on the transfer complete queue is not polled by end of transfer service in last 30 mins" + statistic = "Maximum" + period = "300" + dimensions = { + QueueName = aws_sqs_queue.transfer_complete.name + } + alarm_actions = [data.aws_sns_topic.alarm_notifications.arn] + ok_actions = [data.aws_sns_topic.alarm_notifications.arn] +} + +data "aws_sns_topic" "alarm_notifications" { + name = "${var.environment}-alarm-notifications-sns-topic" +} \ No newline at end of file diff --git a/stacks/suspension-service/terraform/modules/end-of-transfer-service/data.tf b/stacks/suspension-service/terraform/modules/end-of-transfer-service/data.tf new file mode 100644 index 00000000..06565d02 --- /dev/null +++ b/stacks/suspension-service/terraform/modules/end-of-transfer-service/data.tf @@ -0,0 +1,6 @@ +data "aws_ssm_parameter" "transfer_complete_kms_key" { + name = "/repo/${var.environment}/output/ehr-transfer-service/transfer-complete-encryption-kms-key" +} +data "aws_ssm_parameter" "transfer_complete_topic_arn" { + name = "/repo/${var.environment}/output/ehr-transfer-service/transfer-complete-sns-topic-arn" +} \ No newline at end of file diff --git a/stacks/suspension-service/terraform/modules/end-of-transfer-service/iam.tf b/stacks/suspension-service/terraform/modules/end-of-transfer-service/iam.tf new file mode 100644 index 00000000..4ee14a83 --- /dev/null +++ b/stacks/suspension-service/terraform/modules/end-of-transfer-service/iam.tf @@ -0,0 +1,35 @@ +data "aws_iam_policy_document" "transfer_complete_policy_doc" { + statement { + effect = "Allow" + + actions = [ + "sqs:SendMessage" + ] + + principals { + identifiers = ["sns.amazonaws.com"] + type = "Service" + } + + resources = [ + aws_sqs_queue.transfer_complete.arn, + aws_sqs_queue.transfer_complete_observability.arn + ] + + condition { + test = "ArnEquals" + values = [data.aws_ssm_parameter.transfer_complete_topic_arn.value] + variable = "aws:SourceArn" + } + } +} + +resource "aws_sqs_queue_policy" "transfer_complete_observability" { + queue_url = aws_sqs_queue.transfer_complete_observability.id + policy = data.aws_iam_policy_document.transfer_complete_policy_doc.json +} + +resource "aws_sqs_queue_policy" "transfer_complete" { + queue_url = aws_sqs_queue.transfer_complete.id + policy = data.aws_iam_policy_document.transfer_complete_policy_doc.json +} diff --git a/stacks/suspension-service/terraform/modules/end-of-transfer-service/output.tf b/stacks/suspension-service/terraform/modules/end-of-transfer-service/output.tf new file mode 100644 index 00000000..e921bb3b --- /dev/null +++ b/stacks/suspension-service/terraform/modules/end-of-transfer-service/output.tf @@ -0,0 +1,11 @@ +output "end_of_transfer_sns_topic" { + value = nonsensitive(data.aws_ssm_parameter.transfer_complete_topic_arn.value) +} + +output "transfer_complete_queue_name" { + value = aws_sqs_queue.transfer_complete.name +} + +output "transfer_complete_queue_arn" { + value = aws_sqs_queue.transfer_complete.arn +} \ No newline at end of file diff --git a/stacks/suspension-service/terraform/modules/end-of-transfer-service/queue.tf b/stacks/suspension-service/terraform/modules/end-of-transfer-service/queue.tf new file mode 100644 index 00000000..e57cfdc9 --- /dev/null +++ b/stacks/suspension-service/terraform/modules/end-of-transfer-service/queue.tf @@ -0,0 +1,46 @@ +locals { + transfer_complete_queue_name = "${var.environment}-${var.component_name}-transfer-complete" + transfer_complete_observability_queue_name = "${var.environment}-${var.component_name}-transfer-complete-observability" +} + +resource "aws_sqs_queue" "transfer_complete" { + name = local.transfer_complete_queue_name + message_retention_seconds = 259200 + kms_master_key_id = data.aws_ssm_parameter.transfer_complete_kms_key.value + receive_wait_time_seconds = 20 + visibility_timeout_seconds = 240 + + tags = { + Name = local.transfer_complete_queue_name + CreatedBy = var.repo_name + Environment = var.environment + } +} + +resource "aws_sqs_queue" "transfer_complete_observability" { + name = local.transfer_complete_observability_queue_name + message_retention_seconds = 259200 + kms_master_key_id = data.aws_ssm_parameter.transfer_complete_kms_key.value + receive_wait_time_seconds = 20 + visibility_timeout_seconds = 240 + + tags = { + Name = local.transfer_complete_observability_queue_name + CreatedBy = var.repo_name + Environment = var.environment + } +} + +resource "aws_sns_topic_subscription" "transfer_complete_topic" { + protocol = "sqs" + raw_message_delivery = true + topic_arn = data.aws_ssm_parameter.transfer_complete_topic_arn.value + endpoint = aws_sqs_queue.transfer_complete.arn +} + +resource "aws_sns_topic_subscription" "transfer_complete_observability_topic" { + protocol = "sqs" + raw_message_delivery = true + topic_arn = data.aws_ssm_parameter.transfer_complete_topic_arn.value + endpoint = aws_sqs_queue.transfer_complete_observability.arn +} \ No newline at end of file diff --git a/stacks/suspension-service/terraform/modules/end-of-transfer-service/variables.tf b/stacks/suspension-service/terraform/modules/end-of-transfer-service/variables.tf new file mode 100644 index 00000000..2e15cb96 --- /dev/null +++ b/stacks/suspension-service/terraform/modules/end-of-transfer-service/variables.tf @@ -0,0 +1,18 @@ +variable "region" { + type = string + default = "eu-west-2" +} + +variable "repo_name" { + type = string +} + +variable "environment" {} + +variable "component_name" {} + +variable "metric_namespace" {} + +variable "ecs_desired_count" { + default = 0 +} \ No newline at end of file diff --git a/stacks/suspension-service/terraform/tasks b/stacks/suspension-service/terraform/tasks new file mode 100755 index 00000000..63a9f8f4 --- /dev/null +++ b/stacks/suspension-service/terraform/tasks @@ -0,0 +1,274 @@ +#!/usr/bin/env bash + +set -Eeo pipefail + +########################### +# Local Config Parameters # +########################### + +AWS_DEFAULT_REGION=eu-west-2 +IMAGE_REPO_NAME=repo/suspension-service +AWS_HELPERS_VERSION=0.2.27 + +#################################### +# Instance (Environment) Variables # +#################################### + +function check_env { + if [[ -z "${NHS_ENVIRONMENT}" ]]; then + echo "Must set NHS_ENVIRONMENT" + exit 1 + fi + + if [[ -z "${NHS_SERVICE}" ]]; then + echo "Must set NHS_SERVICE" + exit 1 + fi +} + +function set_image_tag() { + if [[ -z "${GO_DEPENDENCY_LABEL_APP}" ]]; then + export IMAGE_TAG=${GO_PIPELINE_LABEL:-$(git rev-parse HEAD | cut -c 1-8)} + else + export IMAGE_TAG=${GO_DEPENDENCY_LABEL_APP} + fi +} + +function get_aws_account_id { + AWS_ACCOUNT_ID=$(dojo -c Dojofile-infra "aws sts get-caller-identity | jq -r .Account") +} + +function configure_envs { + export AWS_REGION=${AWS_DEFAULT_REGION} + export LOCALSTACK_URL="http://localstack:4566" +} + +function configure_sonar_environment_variable { + export SONAR_TOKEN=$(_get_aws_ssm_secret "/repo/dev/output/suspensions-service/sonar_token") +} + +####################### +# Terraform Functions # +####################### + +function tf_init { + check_env + cd terraform + + terraform init -reconfigure \ + -backend-config key="${NHS_SERVICE}-${NHS_ENVIRONMENT}/terraform.tfstate" \ + -backend-config bucket="prm-deductions-${NHS_ENVIRONMENT}-terraform-state" \ + -backend-config dynamodb_table="prm-deductions-${NHS_ENVIRONMENT}-terraform-table" \ + -backend-config region=${AWS_DEFAULT_REGION} +} + +function tf_plan { + operation=$1 + + export IMAGE_TAG="51-4e60390b" + + terraform plan -var task_image_tag=$IMAGE_TAG -var-file="configs/end-of-transfer-service/end-of-transfer-service.tfvars" -var-file="configs/end-of-transfer-service/dev.tfvars" -out="end-of-transfer-service.tfplan" -destroy +} + +function tf_apply { + tf_init + terraform get # modules + terraform apply ${NHS_SERVICE}.tfplan + terraform output -json > tf-out.json +} + +############################ +# Docker Related Functions # +############################ + +function docker_login { + echo Logging in to Amazon ECR... + eval $(dojo -c Dojofile-infra "aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION") +} + +function configure_docker_repository_uri { + docker_login + get_aws_account_id + export REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/$IMAGE_REPO_NAME +} + +function build_docker_image() { + echo Build started on $(date) + set_image_tag + echo Building the Docker image... + docker build --build-arg UTILS_VERSION=$AWS_HELPERS_VERSION -t $REPOSITORY_URI:latest -t $REPOSITORY_URI:$IMAGE_TAG . + echo Build completed on `date` +} + +function configure_local_envs { + export LOG_LEVEL=debug + export NHS_ENVIRONMENT=local +} + +function die_localstack_die { + echo docker processes running: $(docker ps) + + DOCKER_CONTAINERS_ON_PORT=$(docker ps --filter publish=4566 -q) + + echo docker processes running on expected localstack port: $DOCKER_CONTAINERS_ON_PORT + + if [ -z "$DOCKER_CONTAINERS_ON_PORT" ]; then + echo no localstack nonsense going on, cool + else + echo trying to kill localstack... + docker stop $DOCKER_CONTAINERS_ON_PORT + fi +} + +########### +## TASKS ## +########### + +command="$1" +case "${command}" in + fetch_utils) + fetch_redaction_utils + ;; + die_localstack_die) + die_localstack_die + ;; + build_docker) + configure_envs + dojo "./tasks _build" + configure_docker_repository_uri + fetch_redaction_utils + build_docker_image + echo "Pushing the Docker image... $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG" + docker push $REPOSITORY_URI:$IMAGE_TAG + ;; + _build) + rm -rf build/ + gradle assemble + ;; + build) + dojo "./tasks _build" + ;; + _test_unit) + gradle test + ;; + test_unit) + dojo "./tasks _test_unit" + ;; + _test_integration) + gradle --info integration + ;; + test_integration) + configure_envs + die_localstack_die + dojo -c Dojofile-itest "./tasks _test_integration" + ;; + dojo_integration) + configure_envs + dojo -c Dojofile-itest "./tasks _test_integration" + ;; + _test_coverage) + gradle jacocoTestCoverageVerification + ;; + test_coverage) + configure_envs + die_localstack_die + dojo -c Dojofile-itest "./tasks _test_coverage" + ;; + _code_quality) + gradle check -x test -x integration + ;; + code_quality) + die_localstack_die + dojo -c Dojofile-itest "./tasks _code_quality" + ;; + _test_all) + gradle clean test integration jacocoTestCoverageVerification check + ;; + test_all) + configure_envs + die_localstack_die + dojo -c Dojofile-itest "./tasks _test_all" + ;; + run_local) + configure_envs + ./gradlew bootRun + ;; + run_localstack_local) + docker-compose -f docker-compose.localstack-local.yaml up -d + ;; + _tf) + export NHS_SERVICE=suspension-service + + tf_init + bash + ;; + tf) + check_env + dojo -c Dojofile-infra "./tasks _tf" + ;; + _tf_plan_suspension) + export NHS_SERVICE=suspension-service + _assume_environment_role $NHS_ENVIRONMENT + tf_plan "$2" + ;; + tf_plan_suspension) + export NHS_SERVICE=suspension-service + check_env + dojo -c Dojofile-infra "./tasks _tf_plan $2" + ;; + _tf_plan_transfer) + export NHS_SERVICE=end-of-transfer-service + tf_plan "$2" + ;; + tf_plan_transfer) + export NHS_SERVICE=end-of-transfer-service + export NHS_ENVIRONMENT=dev + ./tasks _tf_plan $2 + ;; + _tf_plan) + tf_plan "$2" + ;; + tf_plan) + check_env + dojo -c Dojofile-infra "./tasks _tf_plan $2" + ;; + _tf_apply) + _assume_environment_role $NHS_ENVIRONMENT + tf_apply + ;; + tf_apply) + check_env + dojo -c Dojofile-infra "./tasks _tf_apply" + ;; + promote_docker_image) + check_env + set_image_tag + promote_docker_image "$IMAGE_REPO_NAME:$IMAGE_TAG" "$NHS_ENVIRONMENT" + ;; + _wait_ecs) + _assume_environment_role $NHS_ENVIRONMENT + aws ecs wait services-stable \ + --region $AWS_DEFAULT_REGION \ + --cluster $NHS_ENVIRONMENT-${NHS_SERVICE}-ecs-cluster \ + --service $NHS_ENVIRONMENT-${NHS_SERVICE} + ;; + wait_ecs) + check_env + dojo -c Dojofile-infra "./tasks _wait_ecs" + ;; + _run_sonar) + ./gradlew build sonar --info + ;; + run_sonar) + die_localstack_die + _assume_environment_role $NHS_ENVIRONMENT + configure_envs + configure_sonar_environment_variable + dojo -c Dojofile-itest "./tasks _run_sonar" + ;; + *) + echo "Invalid command: '${command}'" + exit 1 + ;; +esac +set +e From dbb12ffcb9056a6f7ab2922a7fed13e6f7826ce3 Mon Sep 17 00:00:00 2001 From: oliverbeumkes-nhs Date: Thu, 26 Feb 2026 15:19:01 +0000 Subject: [PATCH 03/12] [PRM-726] Override for deploy stack and automated deploy stack to plan a destroy on end-of-transfer-service --- .github/workflows/automated-deploy-stack.yml | 220 +------------------ .github/workflows/deploy-stack.yml | 188 +--------------- 2 files changed, 14 insertions(+), 394 deletions(-) diff --git a/.github/workflows/automated-deploy-stack.yml b/.github/workflows/automated-deploy-stack.yml index 6f34935d..bd8a5ae8 100644 --- a/.github/workflows/automated-deploy-stack.yml +++ b/.github/workflows/automated-deploy-stack.yml @@ -3,43 +3,14 @@ name: "Z-AUTOMATED: Deploy - Stack" on: push: branches: [main] - paths: - - "stacks/deductions/**" - - "stacks/deductions-cross-account/**" - - "stacks/deductions-dashboard/**" - - "stacks/mesh-forwarder/**" - - "stacks/pds-adaptor/**" - - "stacks/re-registration-service/**" + paths: - "stacks/suspension-service/**" - - "stacks/nems-event-processor/**" - - "stacks/ehr-repo/**" - - "stacks/ehr-repo-db-roles/**" - - "stacks/ehr-out-service/**" - - "stacks/ehr-transfer-service/**" - - "stacks/gp2gp-messenger/**" - - "stacks/mhs/**" - - ".github/workflows/automated-deploy-stack.yml" - - ".github/workflows/base-deploy-stack.yml" pull_request: branches: [main] paths: - - "stacks/deductions/**" - - "stacks/deductions-cross-account/**" - - "stacks/deductions-dashboard/**" - - "stacks/mesh-forwarder/**" - - "stacks/pds-adaptor/**" - - "stacks/re-registration-service/**" - "stacks/suspension-service/**" - - "stacks/nems-event-processor/**" - - "stacks/ehr-repo/**" - - "stacks/ehr-repo-db-roles/**" - - "stacks/ehr-out-service/**" - - "stacks/ehr-transfer-service/**" - - "stacks/gp2gp-messenger/**" - - "stacks/mhs/**" - - ".github/workflows/automated-deploy-stack.yml" - - ".github/workflows/base-deploy-stack.yml" + permissions: pull-requests: write @@ -47,195 +18,12 @@ permissions: contents: read # This is required for actions/checkout jobs: - get-changed-files: - runs-on: ubuntu-latest - outputs: - changed: ${{ steps.get-changed-files.outputs.changed }} - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Get changed files - id: get-changed-files - env: - GITHUB_EVENT_NAME: ${{ github.event_name }} - run: | - if [ "$GITHUB_EVENT_NAME" == "push" ]; then - echo "DEBUG: Comparing ${{ github.event.before }} to ${{ github.event.after }}" - changed=$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }} | tr '\n' ' ') - fi - if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then - changed=$(git diff --name-only origin/main... | tr '\n' ' ') - fi - echo "DEBUG: Changed files: '$changed'" - echo "changed=$changed" >> $GITHUB_OUTPUT - - deductions-ci: - if: contains(needs.get-changed-files.outputs.changed, 'stacks/mesh-forwarder') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml') - name: Deductions Infrastructure CI - uses: ./.github/workflows/deploy-stack.yml - needs: [get-changed-files] - with: - stack: deductions - backend_key_alias: deductions-infra-dev - environment: dev - is_deployment: ${{ github.ref == 'refs/heads/main' }} - lambdas_to_build: true - secrets: inherit - - deductions-cross-account: - if: always() && (needs.deductions-ci.result == 'success' || needs.deductions-ci.result == 'skipped') && (contains(needs.get-changed-files.outputs.changed, 'stacks/deductions-cross-account') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml')) - name: deductions-cross-account - uses: ./.github/workflows/deploy-stack.yml - needs: [get-changed-files, deductions-ci] - with: - stack: deductions-cross-account - environment: dev - is_deployment: ${{ github.ref == 'refs/heads/main' }} - backend_key_alias: deductions-infra-cross-account-dev - secrets: inherit - - deductions-dashboard-ci: - if: contains(needs.get-changed-files.outputs.changed, 'stacks/deductions-dashboard') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml') - name: Deductions Dashboard Infrastructure CI - uses: ./.github/workflows/deploy-stack.yml - needs: [get-changed-files, deductions-ci] - with: - stack: deductions-dashboard - backend_key_alias: deductions-infra-dashboard-dev - environment: dev - is_deployment: ${{ github.ref == 'refs/heads/main' }} - secrets: inherit - - mesh-forwarder-ci: - if: contains(needs.get-changed-files.outputs.changed, 'stacks/mesh-forwarder') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml') - name: MESH Forwarder Infrastructure - needs: [get-changed-files, deductions-ci, deductions-cross-account] - uses: ./.github/workflows/deploy-stack.yml - with: - stack: mesh-forwarder - ecr_alias: deductions/mesh-forwarder - environment: dev - is_deployment: ${{ github.ref == 'refs/heads/main' }} - secrets: inherit - - pds-adaptor-ci: - if: contains(needs.get-changed-files.outputs.changed, 'stacks/pds-adaptor') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml') - name: PDS Adaptor Infrastructure - needs: [get-changed-files, deductions-ci, deductions-cross-account] - uses: ./.github/workflows/deploy-stack.yml - with: - stack: pds-adaptor - ecr_alias: deductions/pds-adaptor - environment: dev - is_deployment: ${{ github.ref == 'refs/heads/main' }} - secrets: inherit - - re-registration-service-ci: - if: contains(needs.get-changed-files.outputs.changed, 'stacks/re-registration-service') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml') - name: Re-Registration Service Infrastructure - needs: [get-changed-files, deductions-ci, deductions-cross-account] - uses: ./.github/workflows/deploy-stack.yml - with: - stack: re-registration-service - ecr_alias: repo/re-registration-service - environment: dev - is_deployment: ${{ github.ref == 'refs/heads/main' }} - secrets: inherit - suspension-service-ci: - if: contains(needs.get-changed-files.outputs.changed, 'stacks/suspension-service') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml') - name: Suspension Service Infrastructure - needs: [get-changed-files, deductions-ci, deductions-cross-account] + name: End of Transfer Infrastructure - Destroy plan uses: ./.github/workflows/deploy-stack.yml with: stack: suspension-service ecr_alias: repo/suspension-service environment: dev - is_deployment: ${{ github.ref == 'refs/heads/main' }} - secrets: inherit - - nems-event-processor-ci: - if: contains(needs.get-changed-files.outputs.changed, 'stacks/nems-event-processor') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml') - name: NEMS Event Processor Infrastructure - needs: [get-changed-files, deductions-ci, deductions-cross-account] - uses: ./.github/workflows/deploy-stack.yml - with: - stack: nems-event-processor - ecr_alias: deductions/nems-event-processor - environment: dev - is_deployment: ${{ github.ref == 'refs/heads/main' }} - secrets: inherit - - ehr-repo-ci: - if: contains(needs.get-changed-files.outputs.changed, 'stacks/ehr-repo') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml') - name: EHR Repo Infrastructure CI - uses: ./.github/workflows/deploy-stack.yml - needs: [get-changed-files, deductions-ci, deductions-cross-account] - with: - stack: ehr-repo - ecr_alias: deductions/ehr-repo - environment: dev - is_deployment: ${{ github.ref == 'refs/heads/main' }} - secrets: inherit - - ehr-repo-db-roles-ci: - if: contains(needs.get-changed-files.outputs.changed, 'stacks/ehr-repo') || contains(needs.get-changed-files.outputs.changed, 'stacks/ehr-repo-db-roles') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml') - name: EHR Repo DB Roles Infrastructure CI - uses: ./.github/workflows/deploy-stack.yml - needs: [get-changed-files, deductions-ci, ehr-repo-ci] - with: - stack: ehr-repo-db-roles - environment: dev - backend_key_alias: ehr-repo-dev-db-roles - is_deployment: ${{ github.ref == 'refs/heads/main' }} - secrets: inherit - - ehr-out-service-ci: - if: contains(needs.get-changed-files.outputs.changed, 'stacks/ehr-out-service') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml') - name: EHR Out Service Infrastructure CI - uses: ./.github/workflows/deploy-stack.yml - needs: [get-changed-files, deductions-ci, deductions-cross-account] - with: - stack: ehr-out-service - ecr_alias: deductions/ehr-out-service - environment: dev - is_deployment: ${{ github.ref == 'refs/heads/main' }} - secrets: inherit - - ehr-transfer-service-ci: - if: contains(needs.get-changed-files.outputs.changed, 'stacks/ehr-transfer-service') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml') - name: EHR Transfer Service Infrastructure CI - uses: ./.github/workflows/deploy-stack.yml - needs: [get-changed-files, deductions-ci, deductions-cross-account] - with: - stack: ehr-transfer-service - ecr_alias: deductions/ehr-transfer-service - environment: dev - is_deployment: ${{ github.ref == 'refs/heads/main' }} - secrets: inherit - - gp2gp-messenger-ci: - if: contains(needs.get-changed-files.outputs.changed, 'stacks/gp2gp-messenger') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml') - name: GP2GP Messenger Infrastructure CI - uses: ./.github/workflows/deploy-stack.yml - needs: [get-changed-files, deductions-ci, deductions-cross-account] - with: - stack: gp2gp-messenger - ecr_alias: deductions/gp2gp-messenger - environment: dev - is_deployment: ${{ github.ref == 'refs/heads/main' }} - secrets: inherit - - mhs-ci: - if: contains(needs.get-changed-files.outputs.changed, 'stacks/mhs') || contains(needs.get-changed-files.outputs.changed, '.github/workflows/automated-deploy-stack.yml') - name: MHS Infrastructure CI - uses: ./.github/workflows/deploy-stack.yml - needs: [get-changed-files, deductions-ci, deductions-cross-account] - with: - stack: mhs - backend_key_alias: mhs-dev-repo - environment: dev - is_deployment: ${{ github.ref == 'refs/heads/main' }} + is_deployment: false secrets: inherit diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml index c61cfc46..90808672 100644 --- a/.github/workflows/deploy-stack.yml +++ b/.github/workflows/deploy-stack.yml @@ -1,4 +1,4 @@ -name: Deploy - Stack +name: Destroy end-of-transfer-service infrastructure run-name: "${{ inputs.environment }} | ${{ inputs.stack }} | Terraform Apply? = ${{ inputs.is_deployment }}" @@ -19,129 +19,22 @@ on: required: true type: choice options: - - ehr-repo - - ehr-repo-db-roles - - ehr-out-service - - ehr-transfer-service - - re-registration-service - suspension-service - - nems-event-processor - - deductions - - deductions-backup - - deductions-cross-account - - deductions-dashboard - - mhs - - gp2gp-messenger - - pds-adapter - - mesh-forwarder - - base-infra is_deployment: default: false type: boolean description: "Terraform Apply?" - workflow_call: - inputs: - environment: - default: dev - description: "Environment to deploy to" - type: string - stack: - description: "Stack to deploy - stack folder name" - type: string - backend_key_alias: - description: "Alias for the stack - i.e. deductions is referred to as deductions-infra" - type: string - required: false - ecr_alias: - description: "ECR Repository Alias" - type: string - required: false - is_deployment: - default: false - type: boolean - description: "Terraform Apply?" - ci_account: - default: false - description: "Are we deploying to the CI account?" - type: boolean - lambdas_to_build: - default: false - description: "Do we need to build any lambdas before deploying?" - type: boolean - permissions: pull-requests: write id-token: write # This is required for requesting the JWT contents: read # This is required for actions/checkout jobs: - promote-images-from-source-ecr-account: - environment: ${{ inputs.environment }} - name: Promote images from source to current environment - if: ${{ inputs.is_deployment && inputs.environment != 'dev' && inputs.ecr_alias }} - runs-on: ubuntu-latest - steps: - - name: Checkout - id: checkout - uses: actions/checkout@v6 - - - name: Configure AWS Credentials - id: creds - uses: aws-actions/configure-aws-credentials@v8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 - with: - role-to-assume: ${{ secrets.IAM_ROLE }} - aws-region: ${{ vars.AWS_REGION }} - role-skip-session-tagging: true - mask-aws-account-id: true - - # Needs the AmazonEC2ContainerRegistryPowerUser role - - name: Login to ECR - uses: aws-actions/amazon-ecr-login@v2 - with: - registries: "${{ steps.creds.outputs.aws-account-id }},${{ secrets.AWS_SOURCE_ECR_ACCOUNT_ID }}" - - - name: Get Most Recent ECR Image SHA Tag From Lower Environment - id: get-image-tag - env: - ECR_ALIAS: ${{ inputs.ecr_alias }} - run: | - LATEST_IMAGE_SHA=$(aws ecr describe-images \ - --registry-id ${{ secrets.AWS_SOURCE_ECR_ACCOUNT_ID }} \ - --repository-name $ECR_ALIAS \ - --output json | - jq -r ' - .imageDetails - | map(select(.imageTags != null and (.imageTags|length>0))) - | sort_by(.imagePushedAt) - | last - | .imageTags[] - | select(test("^[0-9a-f]{40}$")) - ' | head -n 1) - - echo "All image tags for $ECR_ALIAS: $LATEST_IMAGE_SHA" - echo "image_sha=$LATEST_IMAGE_SHA" >> $GITHUB_OUTPUT - - - name: ECR Copy - id: ecr-copy - env: - IMAGE_SHA: "${{ steps.get-image-tag.outputs.image_sha }}" - run: | - source_repo=${{ secrets.AWS_SOURCE_ECR_ACCOUNT_ID }}.dkr.ecr.eu-west-2.amazonaws.com/${{ inputs.ecr_alias }} - destination_repo=${{ steps.creds.outputs.aws-account-id }}.dkr.ecr.eu-west-2.amazonaws.com/${{ inputs.ecr_alias }} - - docker pull $source_repo:$IMAGE_SHA - docker tag $source_repo:$IMAGE_SHA $destination_repo:$IMAGE_SHA - docker tag $source_repo:$IMAGE_SHA $destination_repo:${{ github.ref_name }} - docker push $destination_repo:$IMAGE_SHA - docker push $destination_repo:${{ github.ref_name }} - - deploy_stack: - environment: ${{ !inputs.ci_account && inputs.environment || 'ci_account' }} + destroy_stack: + environment: ${{ inputs.environment}} env: - GITHUB_ENV: ${{ !inputs.ci_account && inputs.environment || 'ci_account' }} + GITHUB_ENV: ${{ inputs.environment }} runs-on: ubuntu-latest - needs: [promote-images-from-source-ecr-account] - if: always() && (needs.promote-images-from-source-ecr-account.result == 'skipped' || needs.promote-images-from-source-ecr-account.result == 'success') defaults: run: working-directory: ./stacks/${{ inputs.stack }}/terraform @@ -151,7 +44,7 @@ jobs: with: repository: NHSDigital/orphaned-record-continuity-infrastructure fetch-depth: 0 - ref: ${{ github.ref }} + ref: PRM-726 - name: Configure AWS Credentials (ReadWrite) uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 @@ -162,65 +55,17 @@ jobs: mask-aws-account-id: true role-skip-session-tagging: true - - name: Setup Terraform variables for Deductions Cross Account - if: inputs.stack == 'deductions-cross-account' - env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} - PROMOTION_IAM_ARN: ${{ secrets.PROMOTION_IAM_ARN }} - run: | - cat > pipeline-deductions-cross-account.auto.tfvars <0))) - | sort_by(.imagePushedAt) - | last - | .imageTags[] - | select(test("^[0-9a-f]{40}$")) - ' | head -n 1) - - echo "Found tag: $LATEST_IMAGE_SHA" - - echo "Most recent image tag for $ECR_ALIAS: $LATEST_IMAGE_SHA" - echo "image_tag=$LATEST_IMAGE_SHA" >> $GITHUB_OUTPUT - - name: Setup Terraform variables id: vars env: ECR_ALIAS: ${{ inputs.ecr_alias }} - IMAGE_TAG: ${{ steps.get-image-tag.outputs.image_tag || '' }} + IMAGE_TAG: 51-4e60390b run: | - COMMON_ACCOUNT_ID=$(aws ssm get-parameter --name /repo/ci/user-input/external/aws-account-id --with-decryption | jq -r .Parameter.Value) - cat > pipeline.auto.tfvars <> pipeline.auto.tfvars fi - - name: Build lambdas with additional requirements - if: inputs.lambdas_to_build - run: | - cd ../../../ - ./tasks build_lambdas - - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: @@ -233,25 +78,12 @@ jobs: - name: Terraform Init id: init + env: + KEY: end-of-transfer-service-${{ inputs.environment }} run: | - # compute backend key: prefer alias, otherwise replace '-dev-' with the chosen environment - STACK="${{ inputs.stack }}" - ENV="${{ inputs.environment }}" - ALIAS="${{ inputs.backend_key_alias || '' }}" - - if [ -n "$ALIAS" ]; then - KEY="$ALIAS" - else - if [[ "$STACK" == *"-dev-"* ]]; then - KEY="${STACK/-dev-/-$ENV-}" - else - KEY="${STACK}-$ENV" - fi - fi - echo "Using backend key: $KEY/terraform.tfstate" terraform init -no-color \ - -backend-config="key=${KEY}/terraform.tfstate" \ + -backend-config="key=$KEY/terraform.tfstate" \ -backend-config="bucket=${{ secrets.TF_BACKEND_BUCKET }}" \ -backend-config="dynamodb_table=${{ secrets.TF_BACKEND_TABLE }}" shell: bash @@ -263,7 +95,7 @@ jobs: - name: Terraform Plan id: plan run: | - terraform plan -no-color -input=false -var-file="${{ vars.AWS_ENVIRONMENT }}.tfvars" -out "${{ vars.AWS_ENVIRONMENT }}.tfplan" + terraform plan -var task_image_tag=$IMAGE_TAG -var-file="configs/end-of-transfer-service/end-of-transfer-service.tfvars" -var-file="configs/end-of-transfer-service/dev.tfvars" -out="${{ vars.AWS_ENVIRONMENT }}.tfplan" -destroy terraform show -no-color ${{ vars.AWS_ENVIRONMENT }}.tfplan > ${{ vars.AWS_ENVIRONMENT }}.tfplan.txt echo "summary=$(grep -E 'Plan: [0-9]+ to add, [0-9]+ to change, [0-9]+ to destroy\.|No changes\. Your infrastructure matches the configuration\.' ${{ vars.AWS_ENVIRONMENT }}.tfplan.txt | sed 's/.*No changes\. Your infrastructure matches the configuration/Plan: no changes/g' | sed 's/.*Plan: //g' | sed 's/\..*//g')" >> $GITHUB_OUTPUT shell: bash From f71aad662f90ee10287e6007c1672994072e78b9 Mon Sep 17 00:00:00 2001 From: oliverbeumkes-nhs Date: Thu, 26 Feb 2026 15:22:08 +0000 Subject: [PATCH 04/12] [PRM-726] Reintroduced workflow call for deploy stack --- .github/workflows/deploy-stack.yml | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml index 90808672..de686618 100644 --- a/.github/workflows/deploy-stack.yml +++ b/.github/workflows/deploy-stack.yml @@ -24,7 +24,35 @@ on: default: false type: boolean description: "Terraform Apply?" - + workflow_call: + inputs: + environment: + default: dev + description: "Environment to deploy to" + type: string + stack: + description: "Stack to deploy - stack folder name" + type: string + backend_key_alias: + description: "Alias for the stack - i.e. deductions is referred to as deductions-infra" + type: string + required: false + ecr_alias: + description: "ECR Repository Alias" + type: string + required: false + is_deployment: + default: false + type: boolean + description: "Terraform Apply?" + ci_account: + default: false + description: "Are we deploying to the CI account?" + type: boolean + lambdas_to_build: + default: false + description: "Do we need to build any lambdas before deploying?" + type: boolean permissions: pull-requests: write id-token: write # This is required for requesting the JWT From 8a2728ea27f9ea95d1c4b2447b1b3187d8f2a5be Mon Sep 17 00:00:00 2001 From: oliverbeumkes-nhs Date: Thu, 5 Mar 2026 10:36:56 +0000 Subject: [PATCH 05/12] Removed un used tfvars files and updated plan to use env var --- .github/workflows/deploy-stack.yml | 2 +- .../end-of-transfer-service/perf.tfvars | 8 -- .../end-of-transfer-service/test.tfvars | 8 -- .../suspension-service/terraform/dev.tfvars | 75 ------------------- 4 files changed, 1 insertion(+), 92 deletions(-) delete mode 100644 stacks/suspension-service/terraform/configs/end-of-transfer-service/perf.tfvars delete mode 100644 stacks/suspension-service/terraform/configs/end-of-transfer-service/test.tfvars delete mode 100644 stacks/suspension-service/terraform/dev.tfvars diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml index de686618..210136d6 100644 --- a/.github/workflows/deploy-stack.yml +++ b/.github/workflows/deploy-stack.yml @@ -123,7 +123,7 @@ jobs: - name: Terraform Plan id: plan run: | - terraform plan -var task_image_tag=$IMAGE_TAG -var-file="configs/end-of-transfer-service/end-of-transfer-service.tfvars" -var-file="configs/end-of-transfer-service/dev.tfvars" -out="${{ vars.AWS_ENVIRONMENT }}.tfplan" -destroy + terraform plan -var task_image_tag=$IMAGE_TAG -var-file="configs/end-of-transfer-service/end-of-transfer-service.tfvars" -var-file="configs/end-of-transfer-service/${{ vars.AWS_ENVIRONMENT }}.tfvars" -out="${{ vars.AWS_ENVIRONMENT }}.tfplan" -destroy terraform show -no-color ${{ vars.AWS_ENVIRONMENT }}.tfplan > ${{ vars.AWS_ENVIRONMENT }}.tfplan.txt echo "summary=$(grep -E 'Plan: [0-9]+ to add, [0-9]+ to change, [0-9]+ to destroy\.|No changes\. Your infrastructure matches the configuration\.' ${{ vars.AWS_ENVIRONMENT }}.tfplan.txt | sed 's/.*No changes\. Your infrastructure matches the configuration/Plan: no changes/g' | sed 's/.*Plan: //g' | sed 's/\..*//g')" >> $GITHUB_OUTPUT shell: bash diff --git a/stacks/suspension-service/terraform/configs/end-of-transfer-service/perf.tfvars b/stacks/suspension-service/terraform/configs/end-of-transfer-service/perf.tfvars deleted file mode 100644 index 95c15b4b..00000000 --- a/stacks/suspension-service/terraform/configs/end-of-transfer-service/perf.tfvars +++ /dev/null @@ -1,8 +0,0 @@ -environment = "perf" - -synthetic_patient_prefix = "96936" -process_only_synthetic_patients = false - -log_level = "info" - -ecs_desired_count = 0 \ No newline at end of file diff --git a/stacks/suspension-service/terraform/configs/end-of-transfer-service/test.tfvars b/stacks/suspension-service/terraform/configs/end-of-transfer-service/test.tfvars deleted file mode 100644 index 454adca9..00000000 --- a/stacks/suspension-service/terraform/configs/end-of-transfer-service/test.tfvars +++ /dev/null @@ -1,8 +0,0 @@ -environment = "test" - -synthetic_patient_prefix = "96941" -process_only_synthetic_patients = true - -scale_up_expression = "( (MINUTE(m1)==0 || MINUTE(m1)==15 || MINUTE(m1)==30 || MINUTE(m1)==45 )),10, 0" - -ecs_desired_count = 0 \ No newline at end of file diff --git a/stacks/suspension-service/terraform/dev.tfvars b/stacks/suspension-service/terraform/dev.tfvars deleted file mode 100644 index 8f51384c..00000000 --- a/stacks/suspension-service/terraform/dev.tfvars +++ /dev/null @@ -1,75 +0,0 @@ -environment = "dev" -synthetic_patient_prefix = "96937" -component_name = "suspension-service" -repo_name = "suspension-service" -metric_namespace = "SuspensionService" -can_update_managing_organisation_to_repo = true -repo_process_only_safe_listed_ods_codes = true -process_only_synthetic_patients = false -variable "region" { - type = string - default = "eu-west-2" -} - -variable "repo_name" { - type = string -} - -variable "environment" {} - -variable "component_name" {} - -variable "metric_namespace" {} - -variable "task_image_tag" {} - -variable "task_cpu" { - default = 512 -} -variable "task_memory" { - default = 1024 -} - -variable "log_level" { - type = string - default = "debug" -} - -variable "process_only_synthetic_patients" { - default = true -} - -variable "repo_process_only_safe_listed_ods_codes" { - default = true -} - -variable "synthetic_patient_prefix" {} - -variable "scale_up_expression" { - type = string - default = "((HOUR(m1)==17 && MINUTE(m1)==58)),10,0" -} - -variable "enable_scale_action" { - type = bool - default = true -} - -variable "can_update_managing_organisation_to_repo" { - description = "Toggle to allow updating managing organisation to repo ODS code" - default = false -} - -variable "is_end_of_transfer_service" { - type = bool - default = false -} - -variable "image_name" { - type = string - default = "suspension-service" -} - -variable "ecs_desired_count" { - default = 0 -} \ No newline at end of file From 9ae4aec9307acf36d4a12f78dc857a3d983fa664 Mon Sep 17 00:00:00 2001 From: oliverbeumkes-nhs Date: Thu, 5 Mar 2026 10:37:30 +0000 Subject: [PATCH 06/12] Renamed deploy-stack to destroy-stack --- .github/workflows/deploy-stack.yml | 222 ----------------------------- 1 file changed, 222 deletions(-) delete mode 100644 .github/workflows/deploy-stack.yml diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml deleted file mode 100644 index 210136d6..00000000 --- a/.github/workflows/deploy-stack.yml +++ /dev/null @@ -1,222 +0,0 @@ -name: Destroy end-of-transfer-service infrastructure - -run-name: "${{ inputs.environment }} | ${{ inputs.stack }} | Terraform Apply? = ${{ inputs.is_deployment }}" - -on: - workflow_dispatch: - inputs: - environment: - default: "dev" - description: "Environment" - required: true - type: choice - options: - - dev - - pre-prod - - prod - stack: - description: "Stack" - required: true - type: choice - options: - - suspension-service - is_deployment: - default: false - type: boolean - description: "Terraform Apply?" - workflow_call: - inputs: - environment: - default: dev - description: "Environment to deploy to" - type: string - stack: - description: "Stack to deploy - stack folder name" - type: string - backend_key_alias: - description: "Alias for the stack - i.e. deductions is referred to as deductions-infra" - type: string - required: false - ecr_alias: - description: "ECR Repository Alias" - type: string - required: false - is_deployment: - default: false - type: boolean - description: "Terraform Apply?" - ci_account: - default: false - description: "Are we deploying to the CI account?" - type: boolean - lambdas_to_build: - default: false - description: "Do we need to build any lambdas before deploying?" - type: boolean -permissions: - pull-requests: write - id-token: write # This is required for requesting the JWT - contents: read # This is required for actions/checkout -jobs: - destroy_stack: - environment: ${{ inputs.environment}} - env: - GITHUB_ENV: ${{ inputs.environment }} - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./stacks/${{ inputs.stack }}/terraform - steps: - - name: Check out Repo - uses: actions/checkout@v6 - with: - repository: NHSDigital/orphaned-record-continuity-infrastructure - fetch-depth: 0 - ref: PRM-726 - - - name: Configure AWS Credentials (ReadWrite) - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 - id: creds - with: - role-to-assume: ${{ secrets.IAM_ROLE }} - aws-region: ${{ vars.AWS_REGION }} - mask-aws-account-id: true - role-skip-session-tagging: true - - - name: Setup Terraform variables - id: vars - env: - ECR_ALIAS: ${{ inputs.ecr_alias }} - IMAGE_TAG: 51-4e60390b - run: | - # Only add task_image_tag if stack uses an ECR image - if [ -n "$ECR_ALIAS" ] && [ -n "$IMAGE_TAG" ]; then - echo "task_image_tag = \"${IMAGE_TAG}\"" >> pipeline.auto.tfvars - fi - - - name: Setup Terraform - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: latest - - # - name: Terraform Format - # id: fmt - # run: | - # terraform fmt - - - name: Terraform Init - id: init - env: - KEY: end-of-transfer-service-${{ inputs.environment }} - run: | - echo "Using backend key: $KEY/terraform.tfstate" - terraform init -no-color \ - -backend-config="key=$KEY/terraform.tfstate" \ - -backend-config="bucket=${{ secrets.TF_BACKEND_BUCKET }}" \ - -backend-config="dynamodb_table=${{ secrets.TF_BACKEND_TABLE }}" - shell: bash - - - name: Terraform Validate - id: validate - run: terraform validate -no-color - - - name: Terraform Plan - id: plan - run: | - terraform plan -var task_image_tag=$IMAGE_TAG -var-file="configs/end-of-transfer-service/end-of-transfer-service.tfvars" -var-file="configs/end-of-transfer-service/${{ vars.AWS_ENVIRONMENT }}.tfvars" -out="${{ vars.AWS_ENVIRONMENT }}.tfplan" -destroy - terraform show -no-color ${{ vars.AWS_ENVIRONMENT }}.tfplan > ${{ vars.AWS_ENVIRONMENT }}.tfplan.txt - echo "summary=$(grep -E 'Plan: [0-9]+ to add, [0-9]+ to change, [0-9]+ to destroy\.|No changes\. Your infrastructure matches the configuration\.' ${{ vars.AWS_ENVIRONMENT }}.tfplan.txt | sed 's/.*No changes\. Your infrastructure matches the configuration/Plan: no changes/g' | sed 's/.*Plan: //g' | sed 's/\..*//g')" >> $GITHUB_OUTPUT - shell: bash - - - name: Truncate Plan Output - id: plan-truncated - if: success() || failure() - env: - LENGTH: 64512 - run: | - PLAN_FULL=$(grep -v 'Refreshing state...' <<'EOF' - ${{ steps.plan.outputs.stdout }} - ${{ steps.plan.outputs.stderr }} - EOF - ) - - # Optionally redact sensitive strings in the PLAN_FULL variable - PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's#arn:aws:iam::[0-9]{12}:role/[a-zA-Z0-9_-]+#[REDACTED_IAM_ROLE_ARN]#g') - PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's/[0-9]{12}/[REDACTED_AWS_ACCOUNT_ID]/g') - PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's#https://[a-zA-Z0-9.-]+\.lambda\.amazonaws\.com/[a-zA-Z0-9/._-]+#[REDACTED_LAMBDA_URL]#g') - PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's#https://[a-zA-Z0-9.-]+\.execute-api\.[a-zA-Z0-9.-]+\.amazonaws\.com/[a-zA-Z0-9/._-]*#[REDACTED_API_GATEWAY_URL]#g') - PLAN_FULL=$(echo "$PLAN_FULL" | sed -E '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/s/.*/[REDACTED_PEM_CERT]/') - - echo "PLAN<> $GITHUB_ENV - echo "${PLAN_FULL::$LENGTH}" >> $GITHUB_ENV - [ ${#PLAN_FULL} -gt $LENGTH ] && echo "(truncated - see workflow logs for full output)" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - shell: bash - - - name: Add PR comment - uses: actions/github-script@v8 - if: github.event_name == 'pull_request' && (success() || failure()) - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - // 1. Retrieve existing bot comments for the PR - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }) - const botComment = comments.find(comment => { - return comment.user.type === 'Bot' && comment.body.includes('Report for environment: ndr-dev') - }) - - // 2. Prepare format of the comment - const output = `### Report for ${{ inputs.stack }} - - #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` -
Initialization Output - - \`\`\`\n - ${{ steps.init.outputs.stdout }} - \`\`\` - -
- - #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\` -
Validation Output - - \`\`\`\n - ${{ steps.validate.outputs.stdout }} - \`\`\` - -
- - #### Terraform Plan 📖\`${{ steps.plan.outcome }}\` - -
Show Plan (${{ steps.plan.outputs.summary }}) - - \`\`\`\n - ${{ env.PLAN }} - \`\`\` - -
`; - - // 3. If we have a comment, update it, otherwise create a new one - if (botComment) { - github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: output - }) - } - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: output - }) - - - name: Terraform Apply - if: inputs.is_deployment - run: terraform apply -auto-approve -input=false ${{ vars.AWS_ENVIRONMENT }}.tfplan From 58df7ca7ea5462835d63409c5d38aa21fbdf478b Mon Sep 17 00:00:00 2001 From: oliverbeumkes-nhs Date: Thu, 5 Mar 2026 10:38:51 +0000 Subject: [PATCH 07/12] Renamed deploy-stack to destroy-stack --- .github/workflows/automated-deploy-stack.yml | 2 +- .github/workflows/deploy-full.yml | 169 ------------------- 2 files changed, 1 insertion(+), 170 deletions(-) delete mode 100644 .github/workflows/deploy-full.yml diff --git a/.github/workflows/automated-deploy-stack.yml b/.github/workflows/automated-deploy-stack.yml index bd8a5ae8..3dee651b 100644 --- a/.github/workflows/automated-deploy-stack.yml +++ b/.github/workflows/automated-deploy-stack.yml @@ -20,7 +20,7 @@ permissions: jobs: suspension-service-ci: name: End of Transfer Infrastructure - Destroy plan - uses: ./.github/workflows/deploy-stack.yml + uses: ./.github/workflows/destroy-stack.yml with: stack: suspension-service ecr_alias: repo/suspension-service diff --git a/.github/workflows/deploy-full.yml b/.github/workflows/deploy-full.yml deleted file mode 100644 index aa4f51e8..00000000 --- a/.github/workflows/deploy-full.yml +++ /dev/null @@ -1,169 +0,0 @@ -name: Deploy - Full - -run-name: "${{ inputs.environment }} | Terraform Apply? = ${{ inputs.is_deployment }}" - -on: - workflow_dispatch: - inputs: - environment: - default: "dev" - description: "Environment" - required: true - type: choice - options: - - dev - - pre-prod - - prod - is_deployment: - default: false - type: boolean - description: "Terraform Apply?" - -permissions: - pull-requests: write - id-token: write # This is required for requesting the JWT - contents: read # This is required for actions/checkout - -jobs: - deductions-ci: - name: Deploy Deductions Infrastructure - uses: ./.github/workflows/deploy-stack.yml - with: - stack: deductions - backend_key_alias: deductions-infra-${{ inputs.environment }} - environment: ${{ inputs.environment }} - is_deployment: ${{ inputs.is_deployment }} - lambdas_to_build: true - secrets: inherit - - deductions-cross-account: - name: Deploy Deductions Cross Account Infrastructure - uses: ./.github/workflows/deploy-stack.yml - with: - stack: deductions-cross-account - backend_key_alias: deductions-infra-cross-account-${{ inputs.environment }} - environment: ${{ inputs.environment }} - is_deployment: ${{ inputs.is_deployment }} - lambdas_to_build: true - secrets: inherit - - deductions-dashboard-ci: - name: Deploy Deductions Dashboard Infrastructure - uses: ./.github/workflows/deploy-stack.yml - needs: [deductions-ci] - with: - stack: deductions-dashboard - backend_key_alias: deductions-infra-dashboard-${{ inputs.environment }} - environment: ${{ inputs.environment }} - is_deployment: ${{ inputs.is_deployment }} - secrets: inherit - - mesh-forwarder-ci: - name: Deploy MESH Forwarder Infrastructure - needs: [deductions-ci, deductions-cross-account] - uses: ./.github/workflows/deploy-stack.yml - with: - stack: mesh-forwarder - ecr_alias: deductions/mesh-forwarder - environment: ${{ inputs.environment }} - is_deployment: ${{ inputs.is_deployment }} - secrets: inherit - - pds-adaptor-ci: - name: Deploy PDS Adaptor Infrastructure - needs: [deductions-ci, deductions-cross-account] - uses: ./.github/workflows/deploy-stack.yml - with: - stack: pds-adaptor - ecr_alias: deductions/pds-adaptor - environment: ${{ inputs.environment }} - is_deployment: ${{ inputs.is_deployment }} - secrets: inherit - - re-registration-service-ci: - name: Deploy Re-Registration Service Infrastructure - needs: [deductions-ci, deductions-cross-account] - uses: ./.github/workflows/deploy-stack.yml - with: - stack: re-registration-service - ecr_alias: repo/re-registration-service - environment: ${{ inputs.environment }} - is_deployment: ${{ inputs.is_deployment }} - secrets: inherit - - suspension-service-ci: - name: Deploy Suspension Service Infrastructure - needs: [deductions-ci, deductions-cross-account] - uses: ./.github/workflows/deploy-stack.yml - with: - stack: suspension-service - ecr_alias: repo/suspension-service - environment: ${{ inputs.environment }} - is_deployment: ${{ inputs.is_deployment }} - secrets: inherit - - nems-event-processor-ci: - name: Deploy NEMS Event Processor Infrastructure - needs: [deductions-ci, deductions-cross-account] - uses: ./.github/workflows/deploy-stack.yml - with: - stack: nems-event-processor - ecr_alias: deductions/nems-event-processor - environment: ${{ inputs.environment }} - is_deployment: ${{ inputs.is_deployment }} - secrets: inherit - - ehr-repo-ci: - name: EHR Repo Infrastructure - uses: ./.github/workflows/deploy-stack.yml - needs: [deductions-ci, deductions-cross-account] - with: - stack: ehr-repo - ecr_alias: deductions/ehr-repo - environment: ${{ inputs.environment }} - is_deployment: ${{ inputs.is_deployment }} - secrets: inherit - - ehr-out-service-ci: - name: Deploy EHR Out Service Infrastructure - uses: ./.github/workflows/deploy-stack.yml - needs: [deductions-ci, deductions-cross-account] - with: - stack: ehr-out-service - ecr_alias: deductions/ehr-out-service - environment: ${{ inputs.environment }} - is_deployment: ${{ inputs.is_deployment }} - secrets: inherit - - ehr-transfer-service-ci: - name: Deploy EHR Transfer Service Infrastructure - uses: ./.github/workflows/deploy-stack.yml - needs: [deductions-ci, deductions-cross-account] - with: - stack: ehr-transfer-service - ecr_alias: deductions/ehr-transfer-service - environment: ${{ inputs.environment }} - is_deployment: ${{ inputs.is_deployment }} - secrets: inherit - - gp2gp-messenger-ci: - name: Deploy GP2GP Messenger Infrastructure - uses: ./.github/workflows/deploy-stack.yml - needs: [deductions-ci, deductions-cross-account] - with: - stack: gp2gp-messenger - ecr_alias: deductions/gp2gp-messenger - environment: ${{ inputs.environment }} - is_deployment: ${{ inputs.is_deployment }} - secrets: inherit - - mhs-ci: - name: Deploy MHS Infrastructure - uses: ./.github/workflows/deploy-stack.yml - needs: [deductions-ci, deductions-cross-account] - with: - stack: mhs - backend_key_alias: mhs-${{ inputs.environment }}-repo - environment: ${{ inputs.environment }} - is_deployment: ${{ inputs.is_deployment }} - secrets: inherit From 0c204dee124a51baadd501eb8ba0ea0ba9eea268 Mon Sep 17 00:00:00 2001 From: oliverbeumkes-nhs Date: Thu, 5 Mar 2026 10:39:26 +0000 Subject: [PATCH 08/12] Renamed deploy-stack to destroy-stack --- .github/workflows/destroy-stack.yml | 222 ++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 .github/workflows/destroy-stack.yml diff --git a/.github/workflows/destroy-stack.yml b/.github/workflows/destroy-stack.yml new file mode 100644 index 00000000..210136d6 --- /dev/null +++ b/.github/workflows/destroy-stack.yml @@ -0,0 +1,222 @@ +name: Destroy end-of-transfer-service infrastructure + +run-name: "${{ inputs.environment }} | ${{ inputs.stack }} | Terraform Apply? = ${{ inputs.is_deployment }}" + +on: + workflow_dispatch: + inputs: + environment: + default: "dev" + description: "Environment" + required: true + type: choice + options: + - dev + - pre-prod + - prod + stack: + description: "Stack" + required: true + type: choice + options: + - suspension-service + is_deployment: + default: false + type: boolean + description: "Terraform Apply?" + workflow_call: + inputs: + environment: + default: dev + description: "Environment to deploy to" + type: string + stack: + description: "Stack to deploy - stack folder name" + type: string + backend_key_alias: + description: "Alias for the stack - i.e. deductions is referred to as deductions-infra" + type: string + required: false + ecr_alias: + description: "ECR Repository Alias" + type: string + required: false + is_deployment: + default: false + type: boolean + description: "Terraform Apply?" + ci_account: + default: false + description: "Are we deploying to the CI account?" + type: boolean + lambdas_to_build: + default: false + description: "Do we need to build any lambdas before deploying?" + type: boolean +permissions: + pull-requests: write + id-token: write # This is required for requesting the JWT + contents: read # This is required for actions/checkout +jobs: + destroy_stack: + environment: ${{ inputs.environment}} + env: + GITHUB_ENV: ${{ inputs.environment }} + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./stacks/${{ inputs.stack }}/terraform + steps: + - name: Check out Repo + uses: actions/checkout@v6 + with: + repository: NHSDigital/orphaned-record-continuity-infrastructure + fetch-depth: 0 + ref: PRM-726 + + - name: Configure AWS Credentials (ReadWrite) + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 + id: creds + with: + role-to-assume: ${{ secrets.IAM_ROLE }} + aws-region: ${{ vars.AWS_REGION }} + mask-aws-account-id: true + role-skip-session-tagging: true + + - name: Setup Terraform variables + id: vars + env: + ECR_ALIAS: ${{ inputs.ecr_alias }} + IMAGE_TAG: 51-4e60390b + run: | + # Only add task_image_tag if stack uses an ECR image + if [ -n "$ECR_ALIAS" ] && [ -n "$IMAGE_TAG" ]; then + echo "task_image_tag = \"${IMAGE_TAG}\"" >> pipeline.auto.tfvars + fi + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: latest + + # - name: Terraform Format + # id: fmt + # run: | + # terraform fmt + + - name: Terraform Init + id: init + env: + KEY: end-of-transfer-service-${{ inputs.environment }} + run: | + echo "Using backend key: $KEY/terraform.tfstate" + terraform init -no-color \ + -backend-config="key=$KEY/terraform.tfstate" \ + -backend-config="bucket=${{ secrets.TF_BACKEND_BUCKET }}" \ + -backend-config="dynamodb_table=${{ secrets.TF_BACKEND_TABLE }}" + shell: bash + + - name: Terraform Validate + id: validate + run: terraform validate -no-color + + - name: Terraform Plan + id: plan + run: | + terraform plan -var task_image_tag=$IMAGE_TAG -var-file="configs/end-of-transfer-service/end-of-transfer-service.tfvars" -var-file="configs/end-of-transfer-service/${{ vars.AWS_ENVIRONMENT }}.tfvars" -out="${{ vars.AWS_ENVIRONMENT }}.tfplan" -destroy + terraform show -no-color ${{ vars.AWS_ENVIRONMENT }}.tfplan > ${{ vars.AWS_ENVIRONMENT }}.tfplan.txt + echo "summary=$(grep -E 'Plan: [0-9]+ to add, [0-9]+ to change, [0-9]+ to destroy\.|No changes\. Your infrastructure matches the configuration\.' ${{ vars.AWS_ENVIRONMENT }}.tfplan.txt | sed 's/.*No changes\. Your infrastructure matches the configuration/Plan: no changes/g' | sed 's/.*Plan: //g' | sed 's/\..*//g')" >> $GITHUB_OUTPUT + shell: bash + + - name: Truncate Plan Output + id: plan-truncated + if: success() || failure() + env: + LENGTH: 64512 + run: | + PLAN_FULL=$(grep -v 'Refreshing state...' <<'EOF' + ${{ steps.plan.outputs.stdout }} + ${{ steps.plan.outputs.stderr }} + EOF + ) + + # Optionally redact sensitive strings in the PLAN_FULL variable + PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's#arn:aws:iam::[0-9]{12}:role/[a-zA-Z0-9_-]+#[REDACTED_IAM_ROLE_ARN]#g') + PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's/[0-9]{12}/[REDACTED_AWS_ACCOUNT_ID]/g') + PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's#https://[a-zA-Z0-9.-]+\.lambda\.amazonaws\.com/[a-zA-Z0-9/._-]+#[REDACTED_LAMBDA_URL]#g') + PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's#https://[a-zA-Z0-9.-]+\.execute-api\.[a-zA-Z0-9.-]+\.amazonaws\.com/[a-zA-Z0-9/._-]*#[REDACTED_API_GATEWAY_URL]#g') + PLAN_FULL=$(echo "$PLAN_FULL" | sed -E '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/s/.*/[REDACTED_PEM_CERT]/') + + echo "PLAN<> $GITHUB_ENV + echo "${PLAN_FULL::$LENGTH}" >> $GITHUB_ENV + [ ${#PLAN_FULL} -gt $LENGTH ] && echo "(truncated - see workflow logs for full output)" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + shell: bash + + - name: Add PR comment + uses: actions/github-script@v8 + if: github.event_name == 'pull_request' && (success() || failure()) + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + // 1. Retrieve existing bot comments for the PR + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }) + const botComment = comments.find(comment => { + return comment.user.type === 'Bot' && comment.body.includes('Report for environment: ndr-dev') + }) + + // 2. Prepare format of the comment + const output = `### Report for ${{ inputs.stack }} + + #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` +
Initialization Output + + \`\`\`\n + ${{ steps.init.outputs.stdout }} + \`\`\` + +
+ + #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\` +
Validation Output + + \`\`\`\n + ${{ steps.validate.outputs.stdout }} + \`\`\` + +
+ + #### Terraform Plan 📖\`${{ steps.plan.outcome }}\` + +
Show Plan (${{ steps.plan.outputs.summary }}) + + \`\`\`\n + ${{ env.PLAN }} + \`\`\` + +
`; + + // 3. If we have a comment, update it, otherwise create a new one + if (botComment) { + github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: output + }) + } + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - name: Terraform Apply + if: inputs.is_deployment + run: terraform apply -auto-approve -input=false ${{ vars.AWS_ENVIRONMENT }}.tfplan From b403679f737e8f3f5a4da96d4369dd1ed2adf932 Mon Sep 17 00:00:00 2001 From: oliverbeumkes-nhs Date: Thu, 5 Mar 2026 10:43:30 +0000 Subject: [PATCH 09/12] Renamed destroy-stack to deploy-stack --- .github/workflows/automated-deploy-stack.yml | 2 +- .github/workflows/destroy-stack.yml | 222 ------------------- 2 files changed, 1 insertion(+), 223 deletions(-) delete mode 100644 .github/workflows/destroy-stack.yml diff --git a/.github/workflows/automated-deploy-stack.yml b/.github/workflows/automated-deploy-stack.yml index 3dee651b..bd8a5ae8 100644 --- a/.github/workflows/automated-deploy-stack.yml +++ b/.github/workflows/automated-deploy-stack.yml @@ -20,7 +20,7 @@ permissions: jobs: suspension-service-ci: name: End of Transfer Infrastructure - Destroy plan - uses: ./.github/workflows/destroy-stack.yml + uses: ./.github/workflows/deploy-stack.yml with: stack: suspension-service ecr_alias: repo/suspension-service diff --git a/.github/workflows/destroy-stack.yml b/.github/workflows/destroy-stack.yml deleted file mode 100644 index 210136d6..00000000 --- a/.github/workflows/destroy-stack.yml +++ /dev/null @@ -1,222 +0,0 @@ -name: Destroy end-of-transfer-service infrastructure - -run-name: "${{ inputs.environment }} | ${{ inputs.stack }} | Terraform Apply? = ${{ inputs.is_deployment }}" - -on: - workflow_dispatch: - inputs: - environment: - default: "dev" - description: "Environment" - required: true - type: choice - options: - - dev - - pre-prod - - prod - stack: - description: "Stack" - required: true - type: choice - options: - - suspension-service - is_deployment: - default: false - type: boolean - description: "Terraform Apply?" - workflow_call: - inputs: - environment: - default: dev - description: "Environment to deploy to" - type: string - stack: - description: "Stack to deploy - stack folder name" - type: string - backend_key_alias: - description: "Alias for the stack - i.e. deductions is referred to as deductions-infra" - type: string - required: false - ecr_alias: - description: "ECR Repository Alias" - type: string - required: false - is_deployment: - default: false - type: boolean - description: "Terraform Apply?" - ci_account: - default: false - description: "Are we deploying to the CI account?" - type: boolean - lambdas_to_build: - default: false - description: "Do we need to build any lambdas before deploying?" - type: boolean -permissions: - pull-requests: write - id-token: write # This is required for requesting the JWT - contents: read # This is required for actions/checkout -jobs: - destroy_stack: - environment: ${{ inputs.environment}} - env: - GITHUB_ENV: ${{ inputs.environment }} - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./stacks/${{ inputs.stack }}/terraform - steps: - - name: Check out Repo - uses: actions/checkout@v6 - with: - repository: NHSDigital/orphaned-record-continuity-infrastructure - fetch-depth: 0 - ref: PRM-726 - - - name: Configure AWS Credentials (ReadWrite) - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 - id: creds - with: - role-to-assume: ${{ secrets.IAM_ROLE }} - aws-region: ${{ vars.AWS_REGION }} - mask-aws-account-id: true - role-skip-session-tagging: true - - - name: Setup Terraform variables - id: vars - env: - ECR_ALIAS: ${{ inputs.ecr_alias }} - IMAGE_TAG: 51-4e60390b - run: | - # Only add task_image_tag if stack uses an ECR image - if [ -n "$ECR_ALIAS" ] && [ -n "$IMAGE_TAG" ]; then - echo "task_image_tag = \"${IMAGE_TAG}\"" >> pipeline.auto.tfvars - fi - - - name: Setup Terraform - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: latest - - # - name: Terraform Format - # id: fmt - # run: | - # terraform fmt - - - name: Terraform Init - id: init - env: - KEY: end-of-transfer-service-${{ inputs.environment }} - run: | - echo "Using backend key: $KEY/terraform.tfstate" - terraform init -no-color \ - -backend-config="key=$KEY/terraform.tfstate" \ - -backend-config="bucket=${{ secrets.TF_BACKEND_BUCKET }}" \ - -backend-config="dynamodb_table=${{ secrets.TF_BACKEND_TABLE }}" - shell: bash - - - name: Terraform Validate - id: validate - run: terraform validate -no-color - - - name: Terraform Plan - id: plan - run: | - terraform plan -var task_image_tag=$IMAGE_TAG -var-file="configs/end-of-transfer-service/end-of-transfer-service.tfvars" -var-file="configs/end-of-transfer-service/${{ vars.AWS_ENVIRONMENT }}.tfvars" -out="${{ vars.AWS_ENVIRONMENT }}.tfplan" -destroy - terraform show -no-color ${{ vars.AWS_ENVIRONMENT }}.tfplan > ${{ vars.AWS_ENVIRONMENT }}.tfplan.txt - echo "summary=$(grep -E 'Plan: [0-9]+ to add, [0-9]+ to change, [0-9]+ to destroy\.|No changes\. Your infrastructure matches the configuration\.' ${{ vars.AWS_ENVIRONMENT }}.tfplan.txt | sed 's/.*No changes\. Your infrastructure matches the configuration/Plan: no changes/g' | sed 's/.*Plan: //g' | sed 's/\..*//g')" >> $GITHUB_OUTPUT - shell: bash - - - name: Truncate Plan Output - id: plan-truncated - if: success() || failure() - env: - LENGTH: 64512 - run: | - PLAN_FULL=$(grep -v 'Refreshing state...' <<'EOF' - ${{ steps.plan.outputs.stdout }} - ${{ steps.plan.outputs.stderr }} - EOF - ) - - # Optionally redact sensitive strings in the PLAN_FULL variable - PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's#arn:aws:iam::[0-9]{12}:role/[a-zA-Z0-9_-]+#[REDACTED_IAM_ROLE_ARN]#g') - PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's/[0-9]{12}/[REDACTED_AWS_ACCOUNT_ID]/g') - PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's#https://[a-zA-Z0-9.-]+\.lambda\.amazonaws\.com/[a-zA-Z0-9/._-]+#[REDACTED_LAMBDA_URL]#g') - PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's#https://[a-zA-Z0-9.-]+\.execute-api\.[a-zA-Z0-9.-]+\.amazonaws\.com/[a-zA-Z0-9/._-]*#[REDACTED_API_GATEWAY_URL]#g') - PLAN_FULL=$(echo "$PLAN_FULL" | sed -E '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/s/.*/[REDACTED_PEM_CERT]/') - - echo "PLAN<> $GITHUB_ENV - echo "${PLAN_FULL::$LENGTH}" >> $GITHUB_ENV - [ ${#PLAN_FULL} -gt $LENGTH ] && echo "(truncated - see workflow logs for full output)" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - shell: bash - - - name: Add PR comment - uses: actions/github-script@v8 - if: github.event_name == 'pull_request' && (success() || failure()) - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - // 1. Retrieve existing bot comments for the PR - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }) - const botComment = comments.find(comment => { - return comment.user.type === 'Bot' && comment.body.includes('Report for environment: ndr-dev') - }) - - // 2. Prepare format of the comment - const output = `### Report for ${{ inputs.stack }} - - #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` -
Initialization Output - - \`\`\`\n - ${{ steps.init.outputs.stdout }} - \`\`\` - -
- - #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\` -
Validation Output - - \`\`\`\n - ${{ steps.validate.outputs.stdout }} - \`\`\` - -
- - #### Terraform Plan 📖\`${{ steps.plan.outcome }}\` - -
Show Plan (${{ steps.plan.outputs.summary }}) - - \`\`\`\n - ${{ env.PLAN }} - \`\`\` - -
`; - - // 3. If we have a comment, update it, otherwise create a new one - if (botComment) { - github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: output - }) - } - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: output - }) - - - name: Terraform Apply - if: inputs.is_deployment - run: terraform apply -auto-approve -input=false ${{ vars.AWS_ENVIRONMENT }}.tfplan From 94bcb02d1ac24d09a32fb4268e12eee808854d89 Mon Sep 17 00:00:00 2001 From: oliverbeumkes-nhs Date: Thu, 5 Mar 2026 10:44:50 +0000 Subject: [PATCH 10/12] Renamed destroy-stack to deploy-stack --- .github/workflows/deploy-stack.yml | 222 +++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 .github/workflows/deploy-stack.yml diff --git a/.github/workflows/deploy-stack.yml b/.github/workflows/deploy-stack.yml new file mode 100644 index 00000000..210136d6 --- /dev/null +++ b/.github/workflows/deploy-stack.yml @@ -0,0 +1,222 @@ +name: Destroy end-of-transfer-service infrastructure + +run-name: "${{ inputs.environment }} | ${{ inputs.stack }} | Terraform Apply? = ${{ inputs.is_deployment }}" + +on: + workflow_dispatch: + inputs: + environment: + default: "dev" + description: "Environment" + required: true + type: choice + options: + - dev + - pre-prod + - prod + stack: + description: "Stack" + required: true + type: choice + options: + - suspension-service + is_deployment: + default: false + type: boolean + description: "Terraform Apply?" + workflow_call: + inputs: + environment: + default: dev + description: "Environment to deploy to" + type: string + stack: + description: "Stack to deploy - stack folder name" + type: string + backend_key_alias: + description: "Alias for the stack - i.e. deductions is referred to as deductions-infra" + type: string + required: false + ecr_alias: + description: "ECR Repository Alias" + type: string + required: false + is_deployment: + default: false + type: boolean + description: "Terraform Apply?" + ci_account: + default: false + description: "Are we deploying to the CI account?" + type: boolean + lambdas_to_build: + default: false + description: "Do we need to build any lambdas before deploying?" + type: boolean +permissions: + pull-requests: write + id-token: write # This is required for requesting the JWT + contents: read # This is required for actions/checkout +jobs: + destroy_stack: + environment: ${{ inputs.environment}} + env: + GITHUB_ENV: ${{ inputs.environment }} + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./stacks/${{ inputs.stack }}/terraform + steps: + - name: Check out Repo + uses: actions/checkout@v6 + with: + repository: NHSDigital/orphaned-record-continuity-infrastructure + fetch-depth: 0 + ref: PRM-726 + + - name: Configure AWS Credentials (ReadWrite) + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 + id: creds + with: + role-to-assume: ${{ secrets.IAM_ROLE }} + aws-region: ${{ vars.AWS_REGION }} + mask-aws-account-id: true + role-skip-session-tagging: true + + - name: Setup Terraform variables + id: vars + env: + ECR_ALIAS: ${{ inputs.ecr_alias }} + IMAGE_TAG: 51-4e60390b + run: | + # Only add task_image_tag if stack uses an ECR image + if [ -n "$ECR_ALIAS" ] && [ -n "$IMAGE_TAG" ]; then + echo "task_image_tag = \"${IMAGE_TAG}\"" >> pipeline.auto.tfvars + fi + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: latest + + # - name: Terraform Format + # id: fmt + # run: | + # terraform fmt + + - name: Terraform Init + id: init + env: + KEY: end-of-transfer-service-${{ inputs.environment }} + run: | + echo "Using backend key: $KEY/terraform.tfstate" + terraform init -no-color \ + -backend-config="key=$KEY/terraform.tfstate" \ + -backend-config="bucket=${{ secrets.TF_BACKEND_BUCKET }}" \ + -backend-config="dynamodb_table=${{ secrets.TF_BACKEND_TABLE }}" + shell: bash + + - name: Terraform Validate + id: validate + run: terraform validate -no-color + + - name: Terraform Plan + id: plan + run: | + terraform plan -var task_image_tag=$IMAGE_TAG -var-file="configs/end-of-transfer-service/end-of-transfer-service.tfvars" -var-file="configs/end-of-transfer-service/${{ vars.AWS_ENVIRONMENT }}.tfvars" -out="${{ vars.AWS_ENVIRONMENT }}.tfplan" -destroy + terraform show -no-color ${{ vars.AWS_ENVIRONMENT }}.tfplan > ${{ vars.AWS_ENVIRONMENT }}.tfplan.txt + echo "summary=$(grep -E 'Plan: [0-9]+ to add, [0-9]+ to change, [0-9]+ to destroy\.|No changes\. Your infrastructure matches the configuration\.' ${{ vars.AWS_ENVIRONMENT }}.tfplan.txt | sed 's/.*No changes\. Your infrastructure matches the configuration/Plan: no changes/g' | sed 's/.*Plan: //g' | sed 's/\..*//g')" >> $GITHUB_OUTPUT + shell: bash + + - name: Truncate Plan Output + id: plan-truncated + if: success() || failure() + env: + LENGTH: 64512 + run: | + PLAN_FULL=$(grep -v 'Refreshing state...' <<'EOF' + ${{ steps.plan.outputs.stdout }} + ${{ steps.plan.outputs.stderr }} + EOF + ) + + # Optionally redact sensitive strings in the PLAN_FULL variable + PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's#arn:aws:iam::[0-9]{12}:role/[a-zA-Z0-9_-]+#[REDACTED_IAM_ROLE_ARN]#g') + PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's/[0-9]{12}/[REDACTED_AWS_ACCOUNT_ID]/g') + PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's#https://[a-zA-Z0-9.-]+\.lambda\.amazonaws\.com/[a-zA-Z0-9/._-]+#[REDACTED_LAMBDA_URL]#g') + PLAN_FULL=$(echo "$PLAN_FULL" | sed -E 's#https://[a-zA-Z0-9.-]+\.execute-api\.[a-zA-Z0-9.-]+\.amazonaws\.com/[a-zA-Z0-9/._-]*#[REDACTED_API_GATEWAY_URL]#g') + PLAN_FULL=$(echo "$PLAN_FULL" | sed -E '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/s/.*/[REDACTED_PEM_CERT]/') + + echo "PLAN<> $GITHUB_ENV + echo "${PLAN_FULL::$LENGTH}" >> $GITHUB_ENV + [ ${#PLAN_FULL} -gt $LENGTH ] && echo "(truncated - see workflow logs for full output)" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + shell: bash + + - name: Add PR comment + uses: actions/github-script@v8 + if: github.event_name == 'pull_request' && (success() || failure()) + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + // 1. Retrieve existing bot comments for the PR + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }) + const botComment = comments.find(comment => { + return comment.user.type === 'Bot' && comment.body.includes('Report for environment: ndr-dev') + }) + + // 2. Prepare format of the comment + const output = `### Report for ${{ inputs.stack }} + + #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` +
Initialization Output + + \`\`\`\n + ${{ steps.init.outputs.stdout }} + \`\`\` + +
+ + #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\` +
Validation Output + + \`\`\`\n + ${{ steps.validate.outputs.stdout }} + \`\`\` + +
+ + #### Terraform Plan 📖\`${{ steps.plan.outcome }}\` + +
Show Plan (${{ steps.plan.outputs.summary }}) + + \`\`\`\n + ${{ env.PLAN }} + \`\`\` + +
`; + + // 3. If we have a comment, update it, otherwise create a new one + if (botComment) { + github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: output + }) + } + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + + - name: Terraform Apply + if: inputs.is_deployment + run: terraform apply -auto-approve -input=false ${{ vars.AWS_ENVIRONMENT }}.tfplan From b73c25c8877a0bf15a3b5491149e598925ca8912 Mon Sep 17 00:00:00 2001 From: oliverbeumkes-nhs Date: Thu, 5 Mar 2026 11:11:58 +0000 Subject: [PATCH 11/12] Updated deny_http to use sns_base_arns --- stacks/suspension-service/terraform/sns-topic.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stacks/suspension-service/terraform/sns-topic.tf b/stacks/suspension-service/terraform/sns-topic.tf index c95b0ef7..65edb8d8 100644 --- a/stacks/suspension-service/terraform/sns-topic.tf +++ b/stacks/suspension-service/terraform/sns-topic.tf @@ -99,7 +99,7 @@ data "aws_sns_topic" "alarm_notifications" { } resource "aws_sns_topic_policy" "deny_http" { - for_each = toset(local.sns_arns) + for_each = toset(local.sns_base_arns) arn = each.value From c971cc1ac0e251d52335f5fb04312ad7d06167aa Mon Sep 17 00:00:00 2001 From: chrisbloe Date: Thu, 5 Mar 2026 11:51:30 +0000 Subject: [PATCH 12/12] Changing references to sns_arns just to get the destroy working --- stacks/suspension-service/terraform/iam.tf | 24 +++++++++---------- .../suspension-service/terraform/sns-topic.tf | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/stacks/suspension-service/terraform/iam.tf b/stacks/suspension-service/terraform/iam.tf index 15645dcb..cf51dd76 100644 --- a/stacks/suspension-service/terraform/iam.tf +++ b/stacks/suspension-service/terraform/iam.tf @@ -1,17 +1,17 @@ locals { account_id = data.aws_caller_identity.current.account_id - sns_base_arns = [ - aws_sns_topic.not_suspended.arn, - aws_sns_topic.mof_updated.arn, - aws_sns_topic.mof_not_updated.arn, - aws_sns_topic.invalid_suspension.arn, - aws_sns_topic.invalid_suspension_audit_topic.arn, - aws_sns_topic.deceased_patient.arn, - aws_sns_topic.event_out_of_order.arn, - aws_sns_topic.active_suspensions.arn, - module.end-of-transfer-service[0].end_of_transfer_sns_topic - ] - sns_arns = local.sns_base_arns + sns_base_arns = { + not_suspended = aws_sns_topic.not_suspended.arn + mof_updated = aws_sns_topic.mof_updated.arn + mof_not_updated = aws_sns_topic.mof_not_updated.arn + invalid_suspension = aws_sns_topic.invalid_suspension.arn + invalid_suspension_audit = aws_sns_topic.invalid_suspension_audit_topic.arn + deceased_patient = aws_sns_topic.deceased_patient.arn + event_out_of_order = aws_sns_topic.event_out_of_order.arn + active_suspensions = aws_sns_topic.active_suspensions.arn + end_of_transfer = module.end-of-transfer-service[0].end_of_transfer_sns_topic + } + sns_arns = values(local.sns_base_arns) } data "aws_iam_policy_document" "ecs-assume-role-policy" { diff --git a/stacks/suspension-service/terraform/sns-topic.tf b/stacks/suspension-service/terraform/sns-topic.tf index 65edb8d8..27ae303c 100644 --- a/stacks/suspension-service/terraform/sns-topic.tf +++ b/stacks/suspension-service/terraform/sns-topic.tf @@ -99,7 +99,7 @@ data "aws_sns_topic" "alarm_notifications" { } resource "aws_sns_topic_policy" "deny_http" { - for_each = toset(local.sns_base_arns) + for_each = local.sns_base_arns arn = each.value