From 4ea8febfa3c31ea30df7c3810694b981b5d86e52 Mon Sep 17 00:00:00 2001 From: Brage Gording Date: Thu, 28 Aug 2025 13:40:04 +0100 Subject: [PATCH 01/18] Implement template task definition mechanism - Replace the task definition creation with a teamplate version - The template version will be picked up and modified during the ECS deployment workflow - Remove redundant resources/outputs - We no longer need outputs/s3 buckets since we will be using AWS provided github actions for deployment - Move application-specific variables into a config file (to be used in deployment) - A new SSM resources is introduced that will allow tweaking parameters without redeployment --- config/container_variables.yml | 116 +++++++++++++++++ terraform/app/codedeploy.tf | 74 ----------- terraform/app/ecs.tf | 30 +++-- terraform/app/iam_policy_documents.tf | 10 +- terraform/app/modules/ecs_service/main.tf | 6 +- terraform/app/modules/ecs_service/outputs.tf | 9 -- .../app/modules/ecs_service/variables.tf | 1 - terraform/app/outputs.tf | 30 ----- terraform/app/ssm_parameters.tf | 22 +++- terraform/app/variables.tf | 118 ++---------------- terraform/data_replication/ecs.tf | 1 - terraform/data_replication/outputs.tf | 4 - terraform/data_replication/variables.tf | 44 ------- 13 files changed, 174 insertions(+), 291 deletions(-) create mode 100644 config/container_variables.yml diff --git a/config/container_variables.yml b/config/container_variables.yml new file mode 100644 index 0000000000..6369303cfc --- /dev/null +++ b/config/container_variables.yml @@ -0,0 +1,116 @@ +# Container Variables Configuration +# This file specifies environment-specific variables that will be added to or override +# the environment variables extracted from terraform configuration + +environments: + production: + RAILS_ENV: production + SENTRY_ENVIRONMENT: production + + qa: + RAILS_ENV: staging + SENTRY_ENVIRONMENT: qa + + test: + RAILS_ENV: staging + SENTRY_ENVIRONMENT: test + + preview: + RAILS_ENV: staging + SENTRY_ENVIRONMENT: preview + + training: + RAILS_ENV: staging + SENTRY_ENVIRONMENT: training + + sandbox-alpha: + RAILS_ENV: staging + SENTRY_ENVIRONMENT: sandbox-alpha + + sandbox-beta: + RAILS_ENV: staging + SENTRY_ENVIRONMENT: sandbox-beta + +# Tunable Variables Configuration +# This section specifies environment-specific variables that will be populated into the SSM parameter store +# at /${environment}/envs/ and pulled on task startup. These variables can be modified in the cloud +# console or via the AWS CLI and picked up after tasks are restarted, reducing the need to redeploy. +tunable_vars: + production: + web: + MAVIS__SPLUNK__ENABLED: "true" + MAVIS__CIS2__ENABLED: "true" + MAVIS__ACADEMIC_YEAR_NUMBER_OF_PREPARATION_DAYS: 31 + good-job: + GOOD_JOB_MAX_THREADS: 5 + sidekiq: + SIDEKIQ_CONCURRENCY: 5 + MAVIS__PDS__ENQUEUE_BULK_UPDATES: "true" + MAVIS__PDS__RATE_LIMIT_PER_SECOND: 50 + + qa: + web: + MAVIS__SPLUNK__ENABLED: "true" + MAVIS__CIS2__ENABLED: "false" + MAVIS__ACADEMIC_YEAR_NUMBER_OF_PREPARATION_DAYS: 45 + good-job: + GOOD_JOB_MAX_THREADS: 5 + sidekiq: + SIDEKIQ_CONCURRENCY: 5 + MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" + MAVIS__PDS__RATE_LIMIT_PER_SECOND: 5 + + test: + web: + MAVIS__SPLUNK__ENABLED: "true" + MAVIS__CIS2__ENABLED: "true" + good-job: + GOOD_JOB_MAX_THREADS: 5 + sidekiq: + SIDEKIQ_CONCURRENCY: 5 + MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" + MAVIS__PDS__RATE_LIMIT_PER_SECOND: 5 + + preview: + web: + MAVIS__SPLUNK__ENABLED: "false" + MAVIS__CIS2__ENABLED: "false" + good-job: + GOOD_JOB_MAX_THREADS: 5 + sidekiq: + SIDEKIQ_CONCURRENCY: 5 + MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" + MAVIS__PDS__RATE_LIMIT_PER_SECOND: 5 + + training: + web: + MAVIS__SPLUNK__ENABLED: "false" + MAVIS__CIS2__ENABLED: "false" + good-job: + GOOD_JOB_MAX_THREADS: 5 + sidekiq: + SIDEKIQ_CONCURRENCY: 5 + MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" + MAVIS__PDS__RATE_LIMIT_PER_SECOND: 5 + + sandbox-alpha: + web: + MAVIS__SPLUNK__ENABLED: "false" + MAVIS__CIS2__ENABLED: "false" + good-job: + GOOD_JOB_MAX_THREADS: 5 + sidekiq: + SIDEKIQ_CONCURRENCY: 5 + MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" + MAVIS__PDS__RATE_LIMIT_PER_SECOND: 5 + + sandbox-beta: + web: + MAVIS__SPLUNK__ENABLED: "false" + MAVIS__CIS2__ENABLED: "false" + good-job: + GOOD_JOB_MAX_THREADS: 5 + sidekiq: + SIDEKIQ_CONCURRENCY: 5 + MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" + MAVIS__PDS__RATE_LIMIT_PER_SECOND: 5 diff --git a/terraform/app/codedeploy.tf b/terraform/app/codedeploy.tf index 3a4190ef09..8e1b79ab71 100644 --- a/terraform/app/codedeploy.tf +++ b/terraform/app/codedeploy.tf @@ -51,77 +51,3 @@ resource "aws_codedeploy_deployment_group" "blue_green_deployment_group" { } } } - -resource "aws_s3_bucket" "code_deploy_bucket" { - bucket = var.appspec_bucket - force_destroy = true -} - - -data "aws_s3_bucket" "logs" { - bucket = var.access_logs_bucket -} - -resource "aws_s3_bucket_logging" "example" { - bucket = aws_s3_bucket.code_deploy_bucket.id - - target_bucket = data.aws_s3_bucket.logs.id - target_prefix = "codedeploy-log-${var.environment}/" -} - -resource "aws_s3_bucket_versioning" "code_deploy_bucket_versioning" { - bucket = aws_s3_bucket.code_deploy_bucket.id - versioning_configuration { - status = "Enabled" - } -} - -resource "aws_s3_bucket_policy" "block_http" { - bucket = aws_s3_bucket.code_deploy_bucket.id - policy = jsonencode({ - Version = "2012-10-17" - Id = "block-http-policy" - Statement = [ - { - Sid = "HTTPSOnly" - Effect = "Deny" - Principal = { - "AWS" : "*" - } - Action = "s3:*" - Resource = [ - aws_s3_bucket.code_deploy_bucket.arn, - "${aws_s3_bucket.code_deploy_bucket.arn}/*", - ] - Condition = { - Bool = { - "aws:SecureTransport" = "false" - } - } - }, - ] - }) -} - -resource "aws_s3_bucket_public_access_block" "s3_bucket_access" { - bucket = aws_s3_bucket.code_deploy_bucket.bucket - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -resource "aws_s3_object" "appspec_object" { - bucket = aws_s3_bucket.code_deploy_bucket.bucket - key = "appspec.yaml" - acl = "private" - content = templatefile("templates/appspec.yaml.tpl", { - task_definition_arn = module.web_service.task_definition.arn - container_name = module.web_service.task_definition.container_name - container_port = aws_lb_target_group.blue.port - }) - - tags = { - UseWithCodeDeploy = true - } -} diff --git a/terraform/app/ecs.tf b/terraform/app/ecs.tf index 47f3642243..4b9ca4b09e 100644 --- a/terraform/app/ecs.tf +++ b/terraform/app/ecs.tf @@ -22,11 +22,15 @@ resource "aws_ecs_cluster" "cluster" { module "web_service" { source = "./modules/ecs_service" task_config = { - environment = local.task_envs - secrets = local.task_secrets + environment = local.task_envs + secrets = concat( + local.task_secrets, + [{ + name = "ENV_VARS" + valueFrom = aws_ssm_parameter.cloud_variables["web"].arn + }]) cpu = 1024 memory = 2048 - docker_image = "${var.account_id}.dkr.ecr.eu-west-2.amazonaws.com/${var.docker_image}@${var.image_digest}" execution_role_arn = aws_iam_role.ecs_task_execution_role.arn task_role_arn = aws_iam_role.ecs_task_role.arn log_group_name = aws_cloudwatch_log_group.ecs_log_group.name @@ -61,11 +65,15 @@ module "web_service" { module "good_job_service" { source = "./modules/ecs_service" task_config = { - environment = local.task_envs - secrets = local.task_secrets + environment = local.task_envs + secrets = concat( + local.task_secrets, + [{ + name = "ENV_VARS" + valueFrom = aws_ssm_parameter.cloud_variables["good-job"].arn + }]) cpu = 1024 memory = 2048 - docker_image = "${var.account_id}.dkr.ecr.eu-west-2.amazonaws.com/${var.docker_image}@${var.image_digest}" execution_role_arn = aws_iam_role.ecs_task_execution_role.arn task_role_arn = aws_iam_role.ecs_task_role.arn log_group_name = aws_cloudwatch_log_group.ecs_log_group.name @@ -87,11 +95,15 @@ module "good_job_service" { module "sidekiq_service" { source = "./modules/ecs_service" task_config = { - environment = local.task_envs - secrets = local.task_secrets + environment = local.task_envs + secrets = concat( + local.task_secrets, + [{ + name = "ENV_VARS" + valueFrom = aws_ssm_parameter.cloud_variables["sidekiq"].arn + }]) cpu = 1024 memory = 2048 - docker_image = "${var.account_id}.dkr.ecr.eu-west-2.amazonaws.com/${var.docker_image}@${var.image_digest}" execution_role_arn = aws_iam_role.ecs_task_execution_role.arn task_role_arn = aws_iam_role.ecs_task_role.arn log_group_name = aws_cloudwatch_log_group.ecs_log_group.name diff --git a/terraform/app/iam_policy_documents.tf b/terraform/app/iam_policy_documents.tf index 622073eb80..3ec600e94c 100644 --- a/terraform/app/iam_policy_documents.tf +++ b/terraform/app/iam_policy_documents.tf @@ -49,11 +49,13 @@ data "aws_iam_policy_document" "shell_access" { data "aws_iam_policy_document" "ecs_secrets_access" { statement { - sid = "railsKeySid" + sid = "ssmParameterStoreAccessSid" actions = ["ssm:GetParameters"] - resources = concat([ - "arn:aws:ssm:${var.region}:${var.account_id}:parameter${var.rails_master_key_path}" - ], local.parameter_store_arns) + resources = concat( + ["arn:aws:ssm:${var.region}:${var.account_id}:parameter${var.rails_master_key_path}"], + local.parameter_store_arns, #TODO: Remove once all variables are sourced from application config + [for key, value in aws_ssm_parameter.cloud_variables : value.arn] + ) effect = "Allow" } statement { diff --git a/terraform/app/modules/ecs_service/main.tf b/terraform/app/modules/ecs_service/main.tf index 92647cd96d..21e3f35ef3 100644 --- a/terraform/app/modules/ecs_service/main.tf +++ b/terraform/app/modules/ecs_service/main.tf @@ -70,7 +70,7 @@ resource "aws_ecs_service" "this" { } resource "aws_ecs_task_definition" "this" { - family = "mavis-${local.server_type_name}-task-definition-${var.environment}" + family = "mavis-${local.server_type_name}-task-definition-${var.environment}-template" requires_compatibilities = ["FARGATE"] network_mode = "awsvpc" cpu = var.task_config.cpu @@ -80,7 +80,7 @@ resource "aws_ecs_task_definition" "this" { container_definitions = jsonencode([ { name = var.container_name - image = var.task_config.docker_image + image = "CHANGE_ME" essential = true readonlyRootFileSystem = true portMappings = [ @@ -108,4 +108,4 @@ resource "aws_ecs_task_definition" "this" { } } ]) -} +} \ No newline at end of file diff --git a/terraform/app/modules/ecs_service/outputs.tf b/terraform/app/modules/ecs_service/outputs.tf index c91f926f47..efd1121ef8 100644 --- a/terraform/app/modules/ecs_service/outputs.tf +++ b/terraform/app/modules/ecs_service/outputs.tf @@ -10,12 +10,3 @@ output "service" { } description = "Essential attributes of the ECS service" } - -output "task_definition" { - value = { - arn = aws_ecs_task_definition.this.arn - family = aws_ecs_task_definition.this.family - container_name = var.container_name - } - description = "Essential attributes of the ECS task definition" -} diff --git a/terraform/app/modules/ecs_service/variables.tf b/terraform/app/modules/ecs_service/variables.tf index 785da0c6c3..ad11890c72 100644 --- a/terraform/app/modules/ecs_service/variables.tf +++ b/terraform/app/modules/ecs_service/variables.tf @@ -58,7 +58,6 @@ variable "task_config" { })) cpu = number memory = number - docker_image = string execution_role_arn = string task_role_arn = string log_group_name = string diff --git a/terraform/app/outputs.tf b/terraform/app/outputs.tf index cf4458c0d7..beb54847a3 100644 --- a/terraform/app/outputs.tf +++ b/terraform/app/outputs.tf @@ -1,18 +1,3 @@ -output "s3_uri" { - description = "S3 uri for appspec.yaml needed for CodeDeploy" - value = "s3://${aws_s3_bucket.code_deploy_bucket.bucket}/${aws_s3_object.appspec_object.key}" -} - -output "s3_bucket" { - description = "The name of the S3 bucket that stores the appspec.yaml for CodeDeploy" - value = aws_s3_bucket.code_deploy_bucket.bucket -} - -output "s3_key" { - description = "The key of the S3 CodeDeploy appspec object" - value = aws_s3_object.appspec_object.key -} - output "codedeploy_application_name" { description = "The name of the CodeDeploy application" value = aws_codedeploy_app.mavis.name @@ -23,21 +8,6 @@ output "codedeploy_deployment_group_name" { value = aws_codedeploy_deployment_group.blue_green_deployment_group.deployment_group_name } -output "ecs_variables" { - value = { - cluster_name = aws_ecs_cluster.cluster.name - good_job = { - service_name = module.good_job_service.service.name - task_definition = module.good_job_service.task_definition - } - sidekiq = { - service_name = module.sidekiq_service.service.name - task_definition = module.sidekiq_service.task_definition - } - } - description = "Essential attributes of the ECS services" -} - output "db_secret_arn" { description = "The ARN of the secret containing the DB credentials." value = aws_rds_cluster.core.master_user_secret[0].secret_arn diff --git a/terraform/app/ssm_parameters.tf b/terraform/app/ssm_parameters.tf index fcd4e83708..412121dc1b 100644 --- a/terraform/app/ssm_parameters.tf +++ b/terraform/app/ssm_parameters.tf @@ -1,7 +1,25 @@ -resource "aws_ssm_parameter" "environment_config" { +resource "aws_ssm_parameter" "environment_config" { #TODO: Remove once all variables are sourced from application config for_each = local.parameter_store_variables name = "/${var.environment}/env/${each.key}" type = "String" + value = each.value - value = each.value + lifecycle { + ignore_changes = all + } +} + +resource "aws_ssm_parameter" "cloud_variables" { + for_each = toset([ + "web", "good-job", "sidekiq" + ]) + name = "/${var.environment}/envs/${each.value}" + type = "StringList" + value = "service=${each.value}" + + lifecycle { + ignore_changes = [ + value + ] + } } diff --git a/terraform/app/variables.tf b/terraform/app/variables.tf index 92aa246c96..8962f638ca 100644 --- a/terraform/app/variables.tf +++ b/terraform/app/variables.tf @@ -23,12 +23,6 @@ variable "access_logs_bucket" { description = "Name of the S3 bucket which stores access logs for various resources" } -variable "appspec_bucket" { - type = string - description = "Name of the S3 bucket which stores appspec files" - nullable = false -} - variable "account_id" { type = string default = "393416225559" @@ -102,16 +96,6 @@ variable "vpc_log_retention_days" { ########## Task definition configuration ########## -variable "rails_env" { - type = string - default = "staging" - description = "The rails environment configuration to use for the mavis application" - nullable = false - validation { - condition = contains(["staging", "production"], var.rails_env) - error_message = "Incorrect rails environment, allowed values are: {staging, production}" - } -} variable "rails_master_key_path" { type = string @@ -120,61 +104,6 @@ variable "rails_master_key_path" { nullable = false } -variable "docker_image" { - type = string - default = "mavis/webapp" - description = "The docker image name for the essential container in the task definition" - nullable = false -} - -variable "image_digest" { - type = string - description = "The docker image digest for the essential container in the task definition." - nullable = false -} - -variable "enable_cis2" { - type = bool - default = true - description = "Boolean toggle to determine whether the CIS2 feature should be enabled." - nullable = false -} - -variable "enable_pds_enqueue_bulk_updates" { - type = bool - default = true - description = "Whether PDS jobs that update patients in bulk should execute or not. This is disabled in non-production environments to avoid making unnecessary requests to PDS." - nullable = false -} - -variable "academic_year_today_override" { - type = string - default = "nil" - description = "A date that can be used to override today's date when calculating the current academic year." - nullable = false -} - -variable "academic_year_number_of_preparation_days" { - type = number - default = 31 - description = "How many days before the start of the academic year to start the preparation period." - nullable = false -} - -variable "pds_rate_limit_per_second" { - type = number - default = 5 - description = "The rate limit when communicating with PDS." - nullable = false -} - -variable "enable_splunk" { - type = bool - default = true - description = "Boolean toggle to determine whether the Splunk feature should be enabled." - nullable = false -} - variable "enable_enhanced_db_monitoring" { type = bool default = false @@ -182,28 +111,17 @@ variable "enable_enhanced_db_monitoring" { nullable = false } -variable "app_version" { - type = string - description = "The version identifier for the MAVIS application deployment" - default = "Unknown" - nullable = false -} - locals { is_production = var.environment == "production" - parameter_store_variables = tomap({ - MAVIS__ACADEMIC_YEAR_TODAY_OVERRIDE = var.academic_year_today_override - MAVIS__ACADEMIC_YEAR_NUMBER_OF_PREPARATION_DAYS = var.academic_year_number_of_preparation_days - MAVIS__PDS__ENQUEUE_BULK_UPDATES = var.enable_pds_enqueue_bulk_updates ? "true" : "false" - MAVIS__PDS__RATE_LIMIT_PER_SECOND = var.pds_rate_limit_per_second + parameter_store_variables = tomap({ #TODO: Remove once all variables are sourced from application config + MAVIS__ACADEMIC_YEAR_TODAY_OVERRIDE = "" + MAVIS__ACADEMIC_YEAR_NUMBER_OF_PREPARATION_DAYS = "" + MAVIS__PDS__ENQUEUE_BULK_UPDATES = "" + MAVIS__PDS__RATE_LIMIT_PER_SECOND = 5 GOOD_JOB_MAX_THREADS = 5 SIDEKIQ_CONCURRENCY = 5 }) - parameter_store_config_list = [for key, value in local.parameter_store_variables : { - name = key - valueFrom = aws_ssm_parameter.environment_config[key].arn - }] - parameter_store_arns = [for key, value in local.parameter_store_variables : aws_ssm_parameter.environment_config[key].arn] + parameter_store_arns = [for key, value in aws_ssm_parameter.environment_config : value.arn] #TODO: Remove once all variables are sourced from application config sandbox_envs = ( startswith(var.environment, "sandbox") ? [ @@ -223,14 +141,6 @@ locals { name = "DB_NAME" value = aws_rds_cluster.core.database_name }, - { - name = "RAILS_ENV" - value = var.rails_env - }, - { - name = "SENTRY_ENVIRONMENT" - value = var.environment - }, { name = "MAVIS__HOST" value = var.http_hosts.MAVIS__HOST @@ -239,25 +149,13 @@ locals { name = "MAVIS__GIVE_OR_REFUSE_CONSENT_HOST" value = var.http_hosts.MAVIS__GIVE_OR_REFUSE_CONSENT_HOST }, - { - name = "MAVIS__CIS2__ENABLED" - value = var.enable_cis2 ? "true" : "false" - }, - { - name = "MAVIS__SPLUNK__ENABLED" - value = var.enable_splunk ? "true" : "false" - }, - { - name = "APP_VERSION" - value = var.app_version - }, { name = "SIDEKIQ_REDIS_URL" value = "rediss://${aws_elasticache_replication_group.valkey.primary_endpoint_address}:${var.valkey_port}" }, ], local.sandbox_envs) - task_secrets = concat([ + task_secrets = [ { name = "DB_CREDENTIALS" valueFrom = aws_rds_cluster.core.master_user_secret[0].secret_arn @@ -266,7 +164,7 @@ locals { name = "RAILS_MASTER_KEY" valueFrom = var.rails_master_key_path } - ], local.parameter_store_config_list) + ] } ########## RDS configuration ########## diff --git a/terraform/data_replication/ecs.tf b/terraform/data_replication/ecs.tf index 8b6cd87b4c..9e1cb8bbc1 100644 --- a/terraform/data_replication/ecs.tf +++ b/terraform/data_replication/ecs.tf @@ -33,7 +33,6 @@ module "db_access_service" { secrets = local.task_secrets cpu = 1024 memory = 2048 - docker_image = "${var.account_id}.dkr.ecr.eu-west-2.amazonaws.com/${var.docker_image}@${var.image_digest}" execution_role_arn = aws_iam_role.ecs_task_execution_role.arn task_role_arn = aws_iam_role.ecs_task_role.arn log_group_name = aws_cloudwatch_log_group.ecs_log_group.name diff --git a/terraform/data_replication/outputs.tf b/terraform/data_replication/outputs.tf index 6d67f77cfd..e69de29bb2 100644 --- a/terraform/data_replication/outputs.tf +++ b/terraform/data_replication/outputs.tf @@ -1,4 +0,0 @@ -output "task_definition_arn" { - description = "The task definition arn of the db access service" - value = module.db_access_service.task_definition.arn -} diff --git a/terraform/data_replication/variables.tf b/terraform/data_replication/variables.tf index 7bdea5566e..c5ec1e13f3 100644 --- a/terraform/data_replication/variables.tf +++ b/terraform/data_replication/variables.tf @@ -50,30 +50,6 @@ variable "account_id" { nullable = false } -variable "docker_image" { - type = string - default = "mavis/webapp" - description = "The docker image name for the essential container in the task definition" - nullable = false -} - -variable "image_digest" { - type = string - description = "The docker image digest for the essential container in the task definition." - nullable = false -} - -variable "rails_env" { - type = string - default = "staging" - description = "The rails environment configuration to use for the mavis application" - nullable = false - validation { - condition = contains(["staging", "production"], var.rails_env) - error_message = "Incorrect rails environment, allowed values are: {staging, production}" - } -} - variable "rails_master_key_path" { type = string default = "/mavis/staging/credentials/RAILS_MASTER_KEY" @@ -94,26 +70,6 @@ locals { { name = "DB_NAME" value = aws_rds_cluster.cluster.database_name - }, - { - name = "RAILS_ENV" - value = var.rails_env - }, - { - name = "SENTRY_ENVIRONMENT" - value = var.environment - }, - { - name = "MAVIS__CIS2__ENABLED" - value = "false" - }, - { - name = "MAVIS__SPLUNK__ENABLED" - value = "false" - }, - { - name = "MAVIS__PDS__ENQUEUE_BULK_UPDATES" - value = "false" } ] task_secrets = [ From 8d475082d479bf5e751b3a463b181d64dd294e01 Mon Sep 17 00:00:00 2001 From: Brage Gording Date: Thu, 28 Aug 2025 13:52:04 +0100 Subject: [PATCH 02/18] Remove migrated variables out of tfvars - These variables have been moved to a config file to be inserted into the task definition on deploy - Modify the docker-start script to turn the comma separated key-value pairs in the ENV_VARS secret into environment variables --- bin/docker-start | 11 +++++++++++ terraform/app/env/preview.tfvars | 8 -------- terraform/app/env/production.tfvars | 3 --- terraform/app/env/qa.tfvars | 8 -------- terraform/app/env/sandbox-alpha.tfvars | 11 +++-------- terraform/app/env/sandbox-beta.tfvars | 11 +++-------- terraform/app/env/test.tfvars | 3 --- terraform/app/env/training.tfvars | 7 ------- 8 files changed, 17 insertions(+), 45 deletions(-) diff --git a/bin/docker-start b/bin/docker-start index 17fa5448d7..53d19bdcc0 100755 --- a/bin/docker-start +++ b/bin/docker-start @@ -2,6 +2,17 @@ BIN_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +# Extract ENV_VARS into set of variables and export them to the shell +if [ -n "$ENV_VARS" ]; then + IFS=',' read -ra ADDR <<< "$ENV_VARS" + for i in "${ADDR[@]}"; do + if [[ "$i" == *"="* ]]; then + export "${i?}" + echo "Exported: $i" + fi + done +fi + if [ "$SERVER_TYPE" == "web" ]; then echo "Starting web server..." exec "$BIN_DIR"/thrust "$BIN_DIR"/rails server diff --git a/terraform/app/env/preview.tfvars b/terraform/app/env/preview.tfvars index 97df32fa3e..5f36d8227b 100644 --- a/terraform/app/env/preview.tfvars +++ b/terraform/app/env/preview.tfvars @@ -1,26 +1,18 @@ environment = "preview" dns_certificate_arn = null -docker_image = "mavis/webapp" resource_name = { rds_security_group = "mavis-preview-AddonsStack-1PD6PKSN106RK-dbDBClusterSecurityGroup-7cmoQwi6uv8e" loadbalancer = "mavis-preview-pub-lb" lb_security_group = "mavis-preview-PublicHTTPLoadBalancerSecurityGroup-qfHAKWH39OY3" cloudwatch_vpc_log_group = "mavis-preview-FlowLogs" } -rails_env = "staging" rails_master_key_path = "/copilot/mavis/secrets/STAGING_RAILS_MASTER_KEY" -enable_splunk = false -enable_cis2 = false -enable_pds_enqueue_bulk_updates = false - http_hosts = { MAVIS__HOST = "preview.mavistesting.com" MAVIS__GIVE_OR_REFUSE_CONSENT_HOST = "preview.mavistesting.com" } -appspec_bucket = "nhse-mavis-appspec-bucket-preview" - valkey_node_type = "cache.t4g.micro" valkey_log_retention_days = 3 valkey_failover_enabled = false diff --git a/terraform/app/env/production.tfvars b/terraform/app/env/production.tfvars index 527172ef0a..aad4fea6d4 100644 --- a/terraform/app/env/production.tfvars +++ b/terraform/app/env/production.tfvars @@ -1,13 +1,11 @@ environment = "production" dns_certificate_arn = ["arn:aws:acm:eu-west-2:820242920762:certificate/dd00edc0-b305-45bd-83aa-7c7f298b0a68"] -docker_image = "mavis/webapp" resource_name = { rds_security_group = "mavis-production-AddonsStack-H6B1986BQ928-dbDBClusterSecurityGroup-dEt2cEtcHBMo" loadbalancer = "mavis-production-pub-lb" lb_security_group = "mavis-production-PublicHTTPLoadBalancerSecurityGroup-G7umbZTkvkwK" cloudwatch_vpc_log_group = "mavis-production-FlowLogs" } -rails_env = "production" rails_master_key_path = "/copilot/mavis/production/secrets/RAILS_MASTER_KEY" academic_year_number_of_preparation_days = 31 @@ -18,7 +16,6 @@ http_hosts = { MAVIS__GIVE_OR_REFUSE_CONSENT_HOST = "www.give-or-refuse-consent-for-vaccinations.nhs.uk" } -appspec_bucket = "nhse-mavis-appspec-bucket-production" account_id = 820242920762 vpc_log_retention_days = 14 ecs_log_retention_days = 30 diff --git a/terraform/app/env/qa.tfvars b/terraform/app/env/qa.tfvars index a0134184a5..27f1fdb1fe 100644 --- a/terraform/app/env/qa.tfvars +++ b/terraform/app/env/qa.tfvars @@ -1,21 +1,13 @@ environment = "qa" dns_certificate_arn = ["arn:aws:acm:eu-west-2:393416225559:certificate/dafb0f10-ee18-45e2-8971-28d4ab434375"] -docker_image = "mavis/webapp" resource_name = { rds_security_group = "mavis-qa-AddonsStack-Z0L4GX5EUV3I-dbDBClusterSecurityGroup-vd2Avaw4JIgr" loadbalancer = "mavis-qa-pub-lb" lb_security_group = "mavis-qa-PublicHTTPLoadBalancerSecurityGroup-ml4lZT5ey5ih" cloudwatch_vpc_log_group = "mavis-qa-FlowLogs" } -rails_env = "staging" rails_master_key_path = "/copilot/mavis/secrets/STAGING_RAILS_MASTER_KEY" -enable_cis2 = false -enable_pds_enqueue_bulk_updates = false - -# Normally this is 31, but this gives us 2 weeks of additional testing. -academic_year_number_of_preparation_days = 45 - http_hosts = { MAVIS__HOST = "qa.mavistesting.com" MAVIS__GIVE_OR_REFUSE_CONSENT_HOST = "qa.mavistesting.com" diff --git a/terraform/app/env/sandbox-alpha.tfvars b/terraform/app/env/sandbox-alpha.tfvars index b92dcf7197..259710f3d0 100644 --- a/terraform/app/env/sandbox-alpha.tfvars +++ b/terraform/app/env/sandbox-alpha.tfvars @@ -12,16 +12,11 @@ http_hosts = { MAVIS__GIVE_OR_REFUSE_CONSENT_HOST = "sandbox-alpha.mavistesting.com" } -enable_splunk = false -enable_cis2 = false -enable_pds_enqueue_bulk_updates = false - -appspec_bucket = "nhse-mavis-appspec-bucket-sandbox-alpha" -minimum_web_replicas = 1 -maximum_web_replicas = 2 +minimum_web_replicas = 1 +maximum_web_replicas = 2 minimum_sidekiq_replicas = 1 maximum_sidekiq_replicas = 2 -good_job_replicas = 1 +good_job_replicas = 1 valkey_node_type = "cache.t4g.micro" valkey_log_retention_days = 3 diff --git a/terraform/app/env/sandbox-beta.tfvars b/terraform/app/env/sandbox-beta.tfvars index 87644ae9cf..0df3c6881f 100644 --- a/terraform/app/env/sandbox-beta.tfvars +++ b/terraform/app/env/sandbox-beta.tfvars @@ -12,16 +12,11 @@ http_hosts = { MAVIS__GIVE_OR_REFUSE_CONSENT_HOST = "sandbox-beta.mavistesting.com" } -enable_splunk = false -enable_cis2 = false -enable_pds_enqueue_bulk_updates = false - -appspec_bucket = "nhse-mavis-appspec-bucket-sandbox-beta" -minimum_web_replicas = 1 -maximum_web_replicas = 2 +minimum_web_replicas = 1 +maximum_web_replicas = 2 minimum_sidekiq_replicas = 1 maximum_sidekiq_replicas = 2 -good_job_replicas = 1 +good_job_replicas = 1 # Valkey serverless configuration - minimal settings for sandbox valkey_node_type = "cache.t4g.micro" diff --git a/terraform/app/env/test.tfvars b/terraform/app/env/test.tfvars index 1eaae3eb9a..87ad14007f 100644 --- a/terraform/app/env/test.tfvars +++ b/terraform/app/env/test.tfvars @@ -1,13 +1,11 @@ environment = "test" dns_certificate_arn = ["arn:aws:acm:eu-west-2:393416225559:certificate/7e80f006-e9d8-488f-b950-d97f3cc41e4f"] -docker_image = "mavis/webapp" resource_name = { rds_security_group = "mavis-test-AddonsStack-GB8Z9LQVO8OF-dbDBClusterSecurityGroup-1KSO3O1CL4NI5" loadbalancer = "mavis--Publi-W19xy2QLULZ4" lb_security_group = "mavis-test-PublicHTTPLoadBalancerSecurityGroup-15LE48D6JYPML" cloudwatch_vpc_log_group = "mavis-test-FlowLogs" } -rails_env = "staging" rails_master_key_path = "/copilot/mavis/secrets/STAGING_RAILS_MASTER_KEY" # Normally this is 31, but this gives us 2 weeks of additional testing. @@ -17,7 +15,6 @@ http_hosts = { MAVIS__HOST = "test.mavistesting.com" MAVIS__GIVE_OR_REFUSE_CONSENT_HOST = "test.mavistesting.com" } -appspec_bucket = "nhse-mavis-appspec-bucket-test" valkey_node_type = "cache.t4g.micro" valkey_log_retention_days = 3 diff --git a/terraform/app/env/training.tfvars b/terraform/app/env/training.tfvars index 0aa8968a36..6d98882531 100644 --- a/terraform/app/env/training.tfvars +++ b/terraform/app/env/training.tfvars @@ -3,25 +3,18 @@ dns_certificate_arn = [ "arn:aws:acm:eu-west-2:393416225559:certificate/368edbcb-37c5-4146-9087-ff011bef5e05", "arn:aws:acm:eu-west-2:393416225559:certificate/e93e3912-eee4-4f6e-826d-c628bff58527", ] -docker_image = "mavis/webapp" resource_name = { rds_security_group = "mavis-training-AddonsStack-1JZSXP7P84221-dbDBClusterSecurityGroup-A5NL1GFJ83LX" loadbalancer = "mavis--Publi-w1wzc4E2jrl6" lb_security_group = "mavis-training-PublicHTTPLoadBalancerSecurityGroup-L8GOGS04ARYI" cloudwatch_vpc_log_group = "mavis-training-FlowLogs" } -rails_env = "staging" rails_master_key_path = "/copilot/mavis/secrets/STAGING_RAILS_MASTER_KEY" -enable_splunk = false -enable_cis2 = false -enable_pds_enqueue_bulk_updates = false - http_hosts = { MAVIS__HOST = "training.manage-vaccinations-in-schools.nhs.uk" MAVIS__GIVE_OR_REFUSE_CONSENT_HOST = "training.give-or-refuse-consent-for-vaccinations.nhs.uk" } -appspec_bucket = "nhse-mavis-appspec-bucket-training" valkey_node_type = "cache.t4g.micro" valkey_log_retention_days = 3 From bce5f694cebdda35eee8382e4e547071730ca5ff Mon Sep 17 00:00:00 2001 From: Brage Gording Date: Thu, 28 Aug 2025 13:57:27 +0100 Subject: [PATCH 03/18] Create ECS deployment specific role in AWS - Limits permission scope for production deployments - Adheres to the principle of least privilege - Separates permissions between application deployments and infrastructure changes --- terraform/account/deployment_permissions.tf | 38 +++++++++++- .../iam_policy_DeployECSServiceResources.json | 62 +++++++++++++++++++ ...github_trust_policy_development.json.tftpl | 2 +- terraform/account/variables.tf | 5 ++ 4 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 terraform/account/resources/iam_policy_DeployECSServiceResources.json diff --git a/terraform/account/deployment_permissions.tf b/terraform/account/deployment_permissions.tf index e8f9441325..271af618fe 100644 --- a/terraform/account/deployment_permissions.tf +++ b/terraform/account/deployment_permissions.tf @@ -3,7 +3,8 @@ resource "aws_iam_role" "mavis_deploy" { name = "GithubDeployMavisAndInfrastructure" description = "Role allowing terraform deployment from github workflows" assume_role_policy = templatefile("resources/iam_role_github_trust_policy_${var.environment}.json.tftpl", { - account_id = var.account_id + account_id = var.account_id + repository_list = ["repo:nhsuk/manage-vaccinations-in-schools:*"] }) } @@ -27,7 +28,8 @@ resource "aws_iam_role" "data_replication_deploy" { name = "GithubDeployDataReplicationInfrastructure" description = "Role to be assumed by github workflows dealing with the creation and destruction of the data-replication infrastructure." assume_role_policy = templatefile("resources/iam_role_github_trust_policy_${var.environment}.json.tftpl", { - account_id = var.account_id + account_id = var.account_id + repository_list = ["repo:nhsuk/manage-vaccinations-in-schools:*"] }) } @@ -74,7 +76,8 @@ resource "aws_iam_role" "monitoring_deploy" { name = "GithubDeployMonitoring" description = "Role allowing terraform deployment of monitoring resources from github workflows" assume_role_policy = templatefile("resources/iam_role_github_trust_policy_${var.environment}.json.tftpl", { - account_id = var.account_id + account_id = var.account_id + repository_list = ["repo:nhsuk/manage-vaccinations-in-schools:*"] }) } @@ -104,3 +107,32 @@ resource "aws_iam_role_policy_attachment" "mavis_dms" { role = aws_iam_role.mavis_deploy.name policy_arn = aws_iam_policy.dms.arn } + +################ Deploy ECS Service ################ + +resource "aws_iam_role" "deploy_ecs_service" { + name = "GithubDeployECSService" + description = "Role allowing terraform deployment of ECS services from github workflows" + assume_role_policy = templatefile("resources/iam_role_github_trust_policy_${var.environment}.json.tftpl", { + account_id = var.account_id, + repository_list = [ + "repo:nhsuk/manage-vaccinations-in-schools:*", + "repo:NHSDigital/manage-vaccinations-in-schools-reporting:*" + ] + }) +} + +resource "aws_iam_policy" "deploy_ecs_service" { + name = "DeployECSServiceResources" + description = "Permissions for GithubDeployECSService role" + policy = file("resources/iam_policy_DeployECSServiceResources.json") + lifecycle { + ignore_changes = [description] + } +} + +resource "aws_iam_role_policy_attachment" "deploy_ecs_service" { + for_each = local.ecs_deploy_policies + role = aws_iam_role.deploy_ecs_service.name + policy_arn = each.value +} \ No newline at end of file diff --git a/terraform/account/resources/iam_policy_DeployECSServiceResources.json b/terraform/account/resources/iam_policy_DeployECSServiceResources.json new file mode 100644 index 0000000000..b30c6624e3 --- /dev/null +++ b/terraform/account/resources/iam_policy_DeployECSServiceResources.json @@ -0,0 +1,62 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ecs:UpdateService", + "ecs:DescribeServices", + "ecs:ListServices", + "ecs:RegisterTaskDefinition", + "ecs:DeregisterTaskDefinition", + "ecs:DescribeTaskDefinition", + "ecs:ListTaskDefinitions", + "ecs:DescribeClusters", + "ecs:DescribeTasks", + "ecs:DescribeTaskSets", + "ecs:StartTask", + "ecs:ListServiceDeployments", + "ecs:DescribeServiceDeployments", + "ecs:UntagResource", + "ecs:TagResource", + "ecs:ListClusters", + "ecs:ListContainerInstances", + "ecs:ListTaskDefinitionFamilies", + "ecs:ListTasks", + "ecr:PutImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload", + "codedeploy:BatchGetApplicationRevisions", + "codedeploy:BatchGetApplications", + "codedeploy:BatchGetDeploymentGroups", + "codedeploy:BatchGetDeployments", + "codedeploy:ContinueDeployment", + "codedeploy:CreateApplication", + "codedeploy:CreateDeployment", + "codedeploy:CreateDeploymentGroup", + "codedeploy:GetApplication", + "codedeploy:GetApplicationRevision", + "codedeploy:GetDeployment", + "codedeploy:GetDeploymentConfig", + "codedeploy:GetDeploymentGroup", + "codedeploy:GetDeploymentTarget", + "codedeploy:ListApplicationRevisions", + "codedeploy:ListApplications", + "codedeploy:ListDeploymentConfigs", + "codedeploy:ListDeploymentGroups", + "codedeploy:ListDeployments", + "codedeploy:ListDeploymentTargets", + "codedeploy:RegisterApplicationRevision", + "codedeploy:StopDeployment", + "ssm:DescribeParameters", + "ssm:GetParameter", + "ssm:GetParameters", + "ssm:GetParametersByPath", + "ssm:PutParameter", + "iam:PassRole" + ], + "Resource": ["*"] + } + ] +} diff --git a/terraform/account/resources/iam_role_github_trust_policy_development.json.tftpl b/terraform/account/resources/iam_role_github_trust_policy_development.json.tftpl index c8b00eb1b5..d12422de37 100644 --- a/terraform/account/resources/iam_role_github_trust_policy_development.json.tftpl +++ b/terraform/account/resources/iam_role_github_trust_policy_development.json.tftpl @@ -12,7 +12,7 @@ "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }, "StringLike": { - "token.actions.githubusercontent.com:sub": "repo:nhsuk/manage-vaccinations-in-schools:*" + "token.actions.githubusercontent.com:sub": ${jsonencode(repository_list)} } } } diff --git a/terraform/account/variables.tf b/terraform/account/variables.tf index d7a1fa3f3c..f6a14fae64 100644 --- a/terraform/account/variables.tf +++ b/terraform/account/variables.tf @@ -32,4 +32,9 @@ locals { monitoring_policies = merge(local.base_policies, { monitoring_deploy = aws_iam_policy.monitoring_deploy.arn }) + + ecs_deploy_policies = { + ecr_read = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + deploy_ecs_service = aws_iam_policy.deploy_ecs_service.arn + } } From 2c7e91bb562d535fe6453f2d817afe45c67c6c15 Mon Sep 17 00:00:00 2001 From: Brage Gording Date: Thu, 28 Aug 2025 14:00:25 +0100 Subject: [PATCH 04/18] Update deployment workflows - The application can now be deployed independently from infrastructure changes - Create SSM parameter population script in python so it can also be used between services located in different repositories (e.g. like reporting service) - Create a new task definition based on the templated version created by infrastructure deploy - Create appspec.yaml template for handling codedeploys - Remove redundant parameters from terraform deployment --- .github/workflows/deploy-application.yml | 245 +++++++++++--------- .github/workflows/deploy-infrastructure.yml | 34 +-- .github/workflows/deploy.yml | 10 +- .tool-versions | 1 + config/templates/appspec.yaml | 16 ++ script/populate_ssm_parameters.py | 90 +++++++ script/requirements.txt | 2 + 7 files changed, 261 insertions(+), 137 deletions(-) create mode 100644 config/templates/appspec.yaml create mode 100755 script/populate_ssm_parameters.py create mode 100644 script/requirements.txt diff --git a/.github/workflows/deploy-application.yml b/.github/workflows/deploy-application.yml index ba470fa7da..6896a92dd7 100644 --- a/.github/workflows/deploy-application.yml +++ b/.github/workflows/deploy-application.yml @@ -26,6 +26,10 @@ on: - good-job - sidekiq default: all + git_sha_to_deploy: + description: The git commit SHA to deploy. + required: false + type: string workflow_call: inputs: environment: @@ -38,6 +42,10 @@ on: description: The git commit SHA to deploy. required: true type: string + app_version: + description: The git ref to deploy (branch, tag, or commit SHA). + required: false + type: string permissions: {} @@ -46,16 +54,22 @@ concurrency: env: aws-role: ${{ inputs.environment == 'production' - && 'arn:aws:iam::820242920762:role/GithubDeployMavisAndInfrastructure' - || 'arn:aws:iam::393416225559:role/GithubDeployMavisAndInfrastructure' }} + && 'arn:aws:iam::820242920762:role/GithubDeployECSService' + || 'arn:aws:iam::393416225559:role/GithubDeployECSService' }} + aws_account_id: ${{ inputs.environment == 'production' && '820242920762' || '393416225559' }} + cluster_name: mavis-${{ inputs.environment }} + app_version: ${{ inputs.app_version == '' && 'unknown' || inputs.app_version }} jobs: prepare-deployment: name: Prepare deployment runs-on: ubuntu-latest - environment: ${{ inputs.environment }} permissions: id-token: write + strategy: + fail-fast: true + matrix: + service: ${{ inputs.server_types == 'all' && fromJSON('["web", "good-job", "sidekiq"]') || fromJSON(format('["{0}"]', inputs.server_types)) }} steps: - name: Checkout code uses: actions/checkout@v5 @@ -66,150 +80,163 @@ jobs: with: role-to-assume: ${{ env.aws-role }} aws-region: eu-west-2 - - name: Install terraform - uses: hashicorp/setup-terraform@v3 + - name: Setup python + uses: actions/setup-python@v4 with: - terraform_version: 1.11.4 - - name: Get terraform output - id: terraform-output - working-directory: terraform/app + python-version: 3.12.3 + cache: pip + - name: Install Python dependencies + run: python3 -m pip install -r script/requirements.txt + - name: Get image digest + id: get-image-digest + run: | + digest=$(aws ecr describe-images \ + --repository-name mavis/webapp \ + --image-ids imageTag=${{ inputs.git_sha_to_deploy || github.sha }} \ + --query 'imageDetails[0].imageDigest' \ + --output text) + echo "digest=$digest" >> $GITHUB_OUTPUT + - name: Parse environment variables + id: parse-environment-variables run: | - set -e - terraform init -backend-config=env/${{ inputs.environment }}-backend.hcl -reconfigure - terraform output -json | jq -r ' - "s3_bucket=" + .s3_bucket.value, - "s3_key=" + .s3_key.value, - "application=" + .codedeploy_application_name.value, - "application_group=" + .codedeploy_deployment_group_name.value, - "cluster_name=" + .ecs_variables.value.cluster_name, - "good_job_service=" + .ecs_variables.value.good_job.service_name, - "good_job_task_definition=" + .ecs_variables.value.good_job.task_definition.arn, - "sidekiq_service=" + .ecs_variables.value.sidekiq.service_name, - "sidekiq_task_definition=" + .ecs_variables.value.sidekiq.task_definition.arn - ' > ${{ runner.temp }}/DEPLOYMENT_ENVS - - name: Upload Artifact + parsed_env_vars=$(yq -r '.environments.${{ inputs.environment }} | to_entries | .[] | .key + "=" + .value' config/container_variables.yml) + { + echo 'parsed_env_vars<> "$GITHUB_OUTPUT" + - name: Populate web task definition + id: create-task-definition + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition-family: "mavis-${{ matrix.service }}-task-definition-${{ inputs.environment }}-template" + container-name: "application" + image: "${{ env.aws_account_id }}.dkr.ecr.eu-west-2.amazonaws.com/mavis/webapp@${{ steps.get-image-digest.outputs.digest }}" + environment-variables: ${{ steps.parse-environment-variables.outputs.parsed_env_vars }} + - name: Rename task definition file + run: mv ${{ steps.create-task-definition.outputs.task-definition }} ${{ runner.temp }}/${{ matrix.service }}-task-definition.json + - name: Populate SSM parameters for ${{ matrix.service }} service + run: | + python3 script/populate_ssm_parameters.py ${{ inputs.environment }} ${{ matrix.service }} --app-version ${{ env.app_version }} + - name: Upload artifact for ${{ matrix.service }} task definition uses: actions/upload-artifact@v4 with: - name: DEPLOYMENT_ENVS-${{ inputs.environment }} - path: ${{ runner.temp }}/DEPLOYMENT_ENVS + name: ${{ inputs.environment }}-${{ matrix.service }}-task-definition + path: ${{ runner.temp }}/${{ matrix.service }}-task-definition.json - create-web-deployment: - name: Create web deployment + approve-deployments: + name: Wait for approval if required runs-on: ubuntu-latest needs: prepare-deployment - if: inputs.server_types == 'web' || inputs.server_types == 'all' + environment: ${{ inputs.environment }} + steps: + - run: echo "Proceeding with deployment to ${{ inputs.environment }} environment" + + deploy-web: + name: Deploy web service + runs-on: ubuntu-latest + if: ${{ inputs.server_types == 'web' || inputs.server_types == 'all' }} + needs: [prepare-deployment, approve-deployments] permissions: id-token: write steps: - - name: Download artifact - uses: actions/download-artifact@v5 - with: - name: DEPLOYMENT_ENVS-${{ inputs.environment }} - path: ${{ runner.temp }} - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: role-to-assume: ${{ env.aws-role }} aws-region: eu-west-2 - - name: Trigger CodeDeploy deployment + - name: Checkout code + uses: actions/checkout@v5 + - name: Download web task definition artifact + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }} + name: ${{ inputs.environment }}-web-task-definition + - name: Change family of task definition run: | - set -e - source ${{ runner.temp }}/DEPLOYMENT_ENVS - deployment_id=$(aws deploy create-deployment \ - --application-name "$application" --deployment-group-name "$application_group" \ - --s3-location bucket="$s3_bucket",key="$s3_key",bundleType=yaml | jq -r .deploymentId) - echo "Deployment started: $deployment_id" - echo "deployment_id=$deployment_id" >> $GITHUB_ENV - - name: Wait up to 30 minutes for deployment to complete + file_path="${{ runner.temp }}/web-task-definition.json" + family_name="mavis-web-task-definition-${{ inputs.environment }}" + echo "$(jq --arg f "$family_name" '.family = $f' "$file_path")" > "$file_path" + - name: Register web task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: ${{ runner.temp }}/web-task-definition.json + - name: Deploy web service with CodeDeploy + id: deploy-web-service + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: ${{ runner.temp }}/web-task-definition.json + codedeploy-appspec: config/templates/appspec.yaml + cluster: ${{ env.cluster_name }} + service: mavis-${{ inputs.environment }}-web + codedeploy-application: mavis-${{ inputs.environment }} + codedeploy-deployment-group: blue-green-group-${{ inputs.environment }} + - name: Wait for deployment to complete run: | - set -e - aws deploy wait deployment-successful --deployment-id "$deployment_id" + echo "Waiting for CodeDeploy deployment ${{ steps.deploy-web-service.outputs.codedeploy-deployment-id }} to complete..." + aws deploy wait deployment-successful --deployment-id "${{ steps.deploy-web-service.outputs.codedeploy-deployment-id }}" echo "Deployment successful" - create-good-job-deployment: - name: Create good-job deployment + deploy-good-job: + name: Deploy good-job service runs-on: ubuntu-latest - needs: prepare-deployment - if: inputs.server_types == 'good-job' || inputs.server_types == 'all' + if: ${{ inputs.server_types == 'good-job' || inputs.server_types == 'all' }} + needs: [prepare-deployment, approve-deployments] permissions: id-token: write steps: - - name: Download Artifact - uses: actions/download-artifact@v5 - with: - name: DEPLOYMENT_ENVS-${{ inputs.environment }} - path: ${{ runner.temp }} - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ env.aws-role }} aws-region: eu-west-2 - - name: Trigger ECS Deployment - run: | - set -e - source ${{ runner.temp }}/DEPLOYMENT_ENVS - DEPLOYMENT_ID=$(aws ecs update-service --cluster $cluster_name --service $good_job_service \ - --task-definition $good_job_task_definition --force-new-deployment \ - --query 'service.deployments[?rolloutState==`IN_PROGRESS`].[id][0]' --output text) - echo "Deployment started: $DEPLOYMENT_ID" - echo "deployment_id=$DEPLOYMENT_ID" >> $GITHUB_ENV - - name: Wait for deployment to complete + - name: Download good-job task definition artifact + uses: actions/download-artifact@v5 + with: + path: ${{ runner.temp }} + name: ${{ inputs.environment }}-good-job-task-definition + - name: Change family of task definition run: | - set -e - source ${{ runner.temp }}/DEPLOYMENT_ENVS - DEPLOYMENT_STATE=IN_PROGRESS - while [ "$DEPLOYMENT_STATE" == "IN_PROGRESS" ]; do - echo "Waiting for deployment to complete..." - sleep 30 - DEPLOYMENT_STATE="$(aws ecs describe-services --cluster $cluster_name --services $good_job_service \ - --query "services[0].deployments[?id == \`$deployment_id\`].[rolloutState][0]" --output text)" - done - if [ "$DEPLOYMENT_STATE" != "COMPLETED" ]; then - echo "Deployment failed with state: $DEPLOYMENT_STATE" - exit 1 - fi - echo "Deployment successful" + file_path="${{ runner.temp }}/good-job-task-definition.json" + family_name="mavis-good-job-task-definition-${{ inputs.environment }}" + echo "$(jq --arg f "$family_name" '.family = $f' "$file_path")" > "$file_path" + - name: Deploy good-job service + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: ${{ runner.temp }}/good-job-task-definition.json + cluster: ${{ env.cluster_name }} + service: mavis-${{ inputs.environment }}-good-job + force-new-deployment: true + wait-for-service-stability: true create-sidekiq-deployment: name: Create sidekiq deployment runs-on: ubuntu-latest - needs: prepare-deployment - if: inputs.server_types == 'sidekiq' || inputs.server_types == 'all' + if: ${{ inputs.server_types == 'sidekiq' || inputs.server_types == 'all' }} + needs: [prepare-deployment, approve-deployments] permissions: id-token: write steps: - - name: Download Artifact - uses: actions/download-artifact@v5 - with: - name: DEPLOYMENT_ENVS-${{ inputs.environment }} - path: ${{ runner.temp }} - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: role-to-assume: ${{ env.aws-role }} aws-region: eu-west-2 - - name: Trigger ECS Deployment - run: | - set -e - source ${{ runner.temp }}/DEPLOYMENT_ENVS - DEPLOYMENT_ID=$(aws ecs update-service --cluster $cluster_name --service $sidekiq_service \ - --task-definition $sidekiq_task_definition --force-new-deployment \ - --query 'service.deployments[?rolloutState==`IN_PROGRESS`].[id][0]' --output text) - echo "Deployment started: $DEPLOYMENT_ID" - echo "deployment_id=$DEPLOYMENT_ID" >> $GITHUB_ENV - - name: Wait for deployment to complete + - name: Download sidekiq task definition artifact + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }} + name: ${{ inputs.environment }}-sidekiq-task-definition + - name: Change family of task definition run: | - set -e - source ${{ runner.temp }}/DEPLOYMENT_ENVS - DEPLOYMENT_STATE=IN_PROGRESS - while [ "$DEPLOYMENT_STATE" == "IN_PROGRESS" ]; do - echo "Waiting for deployment to complete..." - sleep 30 - DEPLOYMENT_STATE="$(aws ecs describe-services --cluster $cluster_name --services $sidekiq_service \ - --query "services[0].deployments[?id == \`$deployment_id\`].[rolloutState][0]" --output text)" - done - if [ "$DEPLOYMENT_STATE" != "COMPLETED" ]; then - echo "Deployment failed with state: $DEPLOYMENT_STATE" - exit 1 - fi - echo "Deployment successful" + file_path="${{ runner.temp }}/sidekiq-task-definition.json" + family_name="mavis-sidekiq-task-definition-${{ inputs.environment }}" + echo "$(jq --arg f "$family_name" '.family = $f' "$file_path")" > "$file_path" + - name: Deploy sidekiq service + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: ${{ runner.temp }}/sidekiq-task-definition.json + cluster: ${{ env.cluster_name }} + service: mavis-${{ inputs.environment }}-sidekiq + force-new-deployment: true + wait-for-service-stability: true diff --git a/.github/workflows/deploy-infrastructure.yml b/.github/workflows/deploy-infrastructure.yml index 0689550dbe..99d2491f41 100644 --- a/.github/workflows/deploy-infrastructure.yml +++ b/.github/workflows/deploy-infrastructure.yml @@ -8,12 +8,19 @@ on: description: Deployment environment required: true type: string - image_tag: - required: false - type: string git_ref_to_deploy: required: true type: string + workflow_dispatch: + inputs: + environment: + description: Deployment environment + required: true + type: string + git_ref_to_deploy: + description: The git commit SHA to deploy. + required: false + type: string permissions: {} @@ -48,25 +55,6 @@ jobs: with: role-to-assume: ${{ env.aws_role }} aws-region: eu-west-2 - - name: Set image tag - run: | - IMAGE_TAG="${{ inputs.image_tag || github.sha }}" - echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV - - name: Login to ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - name: Pull Docker image - run: | - set -e - DOCKER_IMAGE="${{ steps.login-ecr.outputs.registry }}/mavis/webapp:${IMAGE_TAG}" - docker pull "$DOCKER_IMAGE" - echo "DOCKER_IMAGE=$DOCKER_IMAGE" >> $GITHUB_ENV - - name: Extract image digest - run: | - set -e - DOCKER_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "$DOCKER_IMAGE") - DIGEST="${DOCKER_DIGEST#*@}" - echo "DIGEST=$DIGEST" >> $GITHUB_ENV - name: Install terraform uses: hashicorp/setup-terraform@v3 with: @@ -78,7 +66,7 @@ jobs: run: | set -e terraform init -backend-config="env/${{ inputs.environment }}-backend.hcl" -upgrade - terraform plan -var="image_digest=$DIGEST" -var="app_version=${{ env.git_ref_to_deploy }}" \ + terraform plan \ -var-file="env/${{ inputs.environment }}.tfvars" \ -out ${{ runner.temp }}/tfplan | tee ${{ runner.temp }}/tf_stdout TF_EXIT_CODE=${PIPESTATUS[0]} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 37da0c0f51..bef703aede 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -109,7 +109,7 @@ jobs: update-permissions: runs-on: ubuntu-latest needs: validate-permissions - if: always() && (inputs.environment == 'production' || inputs.environment == 'preview') && needs.validate-permissions.result == 'failure' + if: ${{ !cancelled() && (inputs.environment == 'production' || inputs.environment == 'preview') && needs.validate-permissions.result == 'failure' }} environment: ${{ inputs.environment }} defaults: run: @@ -138,21 +138,21 @@ jobs: validate-permissions, update-permissions, ] - if: always() && + if: ${{ !cancelled() && ((inputs.environment != 'production' && inputs.environment != 'preview') || - needs.validate-permissions.result == 'success' || needs.update-permissions.result == 'success') + needs.validate-permissions.result == 'success' || needs.update-permissions.result == 'success') }} uses: ./.github/workflows/deploy-infrastructure.yml with: environment: ${{ inputs.environment }} - image_tag: ${{ needs.determine-git-sha.outputs.git-sha }} git_ref_to_deploy: ${{ inputs.git_ref_to_deploy || github.ref_name }} deploy-application: permissions: id-token: write needs: [deploy-infrastructure, determine-git-sha] - if: always() && inputs.server_types != 'none' && needs.deploy-infrastructure.result == 'success' + if: ${{ !cancelled() && inputs.server_types != 'none' && needs.deploy-infrastructure.result == 'success' }} uses: ./.github/workflows/deploy-application.yml with: environment: ${{ inputs.environment }} server_types: ${{ inputs.server_types }} git_sha_to_deploy: ${{ needs.determine-git-sha.outputs.git-sha }} + app_version: ${{ inputs.git_ref_to_deploy }} diff --git a/.tool-versions b/.tool-versions index 38af3ad9d7..add02938b1 100644 --- a/.tool-versions +++ b/.tool-versions @@ -3,6 +3,7 @@ hk 1.1.2 nodejs 22.15.0 pkl 0.28.1 postgres 17.2 +python 3.12.3 redis 8.2.1 ruby 3.4.3 terraform 1.11.4 diff --git a/config/templates/appspec.yaml b/config/templates/appspec.yaml new file mode 100644 index 0000000000..bb94bf7cad --- /dev/null +++ b/config/templates/appspec.yaml @@ -0,0 +1,16 @@ +# This is an appspec.yml template file for use with an Amazon ECS deployment in CodeDeploy. +# The lines in this template that start with the hashtag are +# comments that can be safely left in the file or +# ignored. +# For help completing this file, see the "AppSpec File Reference" in the +# "CodeDeploy User Guide" at +# https://docs.aws.amazon.com/codedeploy/latest/userguide/app-spec-ref.html +version: 1.0 +Resources: + - TargetService: + Type: AWS::ECS::Service + Properties: + TaskDefinition: "" + LoadBalancerInfo: + ContainerName: "application" + ContainerPort: "4000" diff --git a/script/populate_ssm_parameters.py b/script/populate_ssm_parameters.py new file mode 100755 index 0000000000..a0e2d996cb --- /dev/null +++ b/script/populate_ssm_parameters.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +import argparse +import sys +import os +import boto3 +import yaml +from typing import Dict, List, Any + + +def load_yaml_config(file_path: str) -> Dict[str, Any]: + """Load and parse YAML configuration file.""" + try: + with open(file_path, 'r') as f: + return yaml.safe_load(f)['tunable_vars'] + except FileNotFoundError: + print(f"Error: Container variables file '{file_path}' not found") + sys.exit(1) + except yaml.YAMLError as e: + print(f"Error: Failed to parse YAML file '{file_path}': {e}") + sys.exit(1) + + +def extract_cloud_variables(config: Dict[str, Any], environment: str, server_type: str) -> List[str]: + """Extract cloud variables from YAML config for the given environment and server type.""" + cloud_vars = [] + + env_config = config[environment] + if server_type in env_config: + for key, value in env_config[server_type].items(): + cloud_vars.append(f"{key}={value}") + + return cloud_vars + + +def update_ssm_parameter(parameter_name: str, values: List[str], app_version: str) -> None: + """Update SSM parameter with StringList values.""" + try: + ssm = boto3.client('ssm') + values.append(f"app_version={app_version}") + string_list = ','.join(values) + + print(f"Updating SSM parameter: {parameter_name}") + + ssm.put_parameter( + Name=parameter_name, + Value=string_list, + Type='StringList', + Overwrite=True + ) + + except Exception as e: + print(f"Error: Failed to update SSM parameter '{parameter_name}': {e}") + sys.exit(1) + + +def main(): + parser = argparse.ArgumentParser( + description="Populate SSM parameter store from cloud_variables configuration", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s sandbox-alpha web + %(prog)s production good-job + """ + ) + + parser.add_argument('environment', help='Environment name (e.g., qa, production, etc.)') + parser.add_argument('server_type', help='Server type') + parser.add_argument('-c', '--config-file', default='config/container_variables.yml', + help='Container variables file path (default: config/container_variables.yml)') + parser.add_argument('--app-version', default='unknown', help='Application version (default: unknown)') + + args = parser.parse_args() + + # Validate config file exists + if not os.path.isfile(args.config_file): + print(f"Error: Container config file '{args.config_file}' not found") + sys.exit(1) + + config = load_yaml_config(args.config_file) + cloud_vars = extract_cloud_variables(config, args.environment, args.server_type) + + ssm_parameter_path = f"/{args.environment}/envs/{args.server_type}" + update_ssm_parameter(ssm_parameter_path, cloud_vars, args.app_version) + + print(f"Cloud variables updated successfully") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/script/requirements.txt b/script/requirements.txt new file mode 100644 index 0000000000..57ceb3f754 --- /dev/null +++ b/script/requirements.txt @@ -0,0 +1,2 @@ +boto3>=1.39.0 +PyYAML>=6.0 \ No newline at end of file From c0e10405d3db4fc685f1837a0d786eaff77ec64c Mon Sep 17 00:00:00 2001 From: Brage Gording Date: Thu, 28 Aug 2025 14:01:09 +0100 Subject: [PATCH 05/18] Also update deployment of data replication - This is required due to the new template-mechanism - Adheres to split between application and infrastructure --- .../workflows/data-replication-pipeline.yml | 274 +++++++----------- .../workflows/refresh-data-replication.yml | 190 ++++++++++++ 2 files changed, 290 insertions(+), 174 deletions(-) create mode 100644 .github/workflows/refresh-data-replication.yml diff --git a/.github/workflows/data-replication-pipeline.yml b/.github/workflows/data-replication-pipeline.yml index 79401d8c2d..113f6e1c41 100644 --- a/.github/workflows/data-replication-pipeline.yml +++ b/.github/workflows/data-replication-pipeline.yml @@ -1,5 +1,5 @@ -name: Data replication pipeline -run-name: ${{ inputs.deployment_type }} for data replication resources for ${{ inputs.environment }} +name: Deploy Data Replication +run-name: Deploy Data Replication service for ${{ inputs.environment }} on: workflow_dispatch: @@ -15,227 +15,153 @@ on: - qa - sandbox-alpha - sandbox-beta - deployment_type: - description: Deployment type - required: true - type: choice - options: - - Deployment with DB recreation - - Application only deployment image_tag: description: Docker image tag to deploy required: false type: string - db_snapshot_arn: - description: ARN of the DB snapshot to use (optional) - required: false - type: string - egress_cidr: - description: CIDR blocks to allow egress traffic. - type: string - required: true - default: "[]" - take_db_snapshot: - description: Take a new DB snapshot before creating the environment - type: boolean - default: false + +permissions: {} env: - aws_role: ${{ inputs.environment == 'production' + aws-role: ${{ inputs.environment == 'production' && 'arn:aws:iam::820242920762:role/GithubDeployDataReplicationInfrastructure' || 'arn:aws:iam::393416225559:role/GithubDeployDataReplicationInfrastructure' }} - db_snapshot_role: ${{ inputs.environment == 'production' - && 'arn:aws:iam::820242920762:role/DatabaseSnapshotRole' - || 'arn:aws:iam::393416225559:role/DatabaseSnapshotRole' }} - -defaults: - run: - working-directory: terraform/data_replication + aws_account_id: ${{ inputs.environment == 'production' && '820242920762' || '393416225559' }} concurrency: group: deploy-data-replica-${{ inputs.environment }} jobs: - prepare-db-replica: - if: ${{ inputs.deployment_type == 'Deployment with DB recreation' }} - name: Prepare data replica + validate-inputs: runs-on: ubuntu-latest - permissions: - id-token: write + permissions: { } steps: - - name: Checkout code - uses: actions/checkout@v5 - - name: Assume DB Snapshot role - if: inputs.take_db_snapshot - uses: aws-actions/configure-aws-credentials@v5 - with: - role-to-assume: ${{ env.db_snapshot_role }} - aws-region: eu-west-2 - - name: Take DB snapshot - if: inputs.take_db_snapshot + - name: Validate inputs run: | - set -e - snapshot_identifier=snapshot-for-data-replication-$(date +"%Y-%m-%d-%H-%M-%S") - aws rds create-db-cluster-snapshot --db-cluster-identifier mavis-${{ inputs.environment }} --db-cluster-snapshot-identifier $snapshot_identifier - echo "Waiting for snapshot to be available. This can take a while." - aws rds wait db-cluster-snapshot-available --db-cluster-snapshot-identifier $snapshot_identifier - echo "New snapshot is now available" - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v5 - with: - role-to-assume: ${{ env.aws_role }} - aws-region: eu-west-2 - - name: Get latest snapshot - id: get-latest-snapshot - run: | - set -e - if [ -z "${{ inputs.db_snapshot_arn }}" ]; then - echo "No snapshot ARN provided, fetching the latest snapshot" - SNAPSHOT_ARN=$(aws rds describe-db-cluster-snapshots \ - --query "DBClusterSnapshots[?DBClusterIdentifier=='mavis-${{ inputs.environment }}'].[DBClusterSnapshotArn, SnapshotCreateTime]" \ - --output text | sort -k2 -r | head -n 1 | cut -f1) - - if [ -z "$SNAPSHOT_ARN" ]; then - echo "No snapshots found for mavis-${{ inputs.environment }}" - exit 1 - fi - else - echo "Using provided snapshot ARN: ${{ inputs.db_snapshot_arn }}" - SNAPSHOT_ARN="${{ inputs.db_snapshot_arn }}" + if [[ "${{ inputs.environment }}" == "preview" || "${{ inputs.environment }}" == "production" ]]; then + if [[ -z "${{ inputs.git_ref_to_deploy }}" ]]; then + echo "Error: git_ref_to_deploy is required for preview and production environments." + exit 1 + fi fi - echo "Using snapshot ARN: $SNAPSHOT_ARN" - echo "SNAPSHOT_ARN=$SNAPSHOT_ARN" >> $GITHUB_OUTPUT - - name: Install terraform - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: 1.11.4 - outputs: - SNAPSHOT_ARN: ${{ steps.get-latest-snapshot.outputs.SNAPSHOT_ARN }} - - prepare-webapp: - name: Prepare webapp + determine-git-sha: runs-on: ubuntu-latest - permissions: - id-token: write + permissions: { } + needs: validate-inputs + outputs: + git-sha: ${{ steps.get-git-sha.outputs.git-sha }} steps: - name: Checkout code uses: actions/checkout@v5 - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v5 with: - role-to-assume: ${{ env.aws_role }} - aws-region: eu-west-2 - - name: ECR login - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - name: Get docker image digest - id: get-docker-image-digest - run: | - set -e - DOCKER_IMAGE="${{ steps.login-ecr.outputs.registry }}/mavis/webapp:${{ inputs.image_tag || github.sha }}" - docker pull "$DOCKER_IMAGE" - DOCKER_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "$DOCKER_IMAGE") - DIGEST="${DOCKER_DIGEST#*@}" - echo "DIGEST=$DIGEST" >> $GITHUB_OUTPUT - outputs: - DOCKER_DIGEST: ${{ steps.get-docker-image-digest.outputs.DIGEST }} + ref: ${{ inputs.git_ref_to_deploy || github.sha }} + - name: Get git sha + id: get-git-sha + run: echo "git-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + build-and-push-image: + permissions: + id-token: write + needs: determine-git-sha + uses: ./.github/workflows/build-and-push-image.yml + with: + git-sha: ${{ needs.determine-git-sha.outputs.git-sha }} - plan: - name: Terraform plan + prepare-deployment: + name: Prepare deployment runs-on: ubuntu-latest - needs: - - prepare-db-replica - - prepare-webapp - if: ${{ !cancelled() && - (needs.prepare-db-replica.result == 'success' || needs.prepare-db-replica.result == 'skipped') && - needs.prepare-webapp.result == 'success' }} - env: - SNAPSHOT_ARN: ${{ needs.prepare-db-replica.outputs.SNAPSHOT_ARN }} - DB_SECRET_ARN: ${{ needs.prepare-db-replica.outputs.DB_SECRET_ARN || 'arn:aws:secretsmanager:eu-west-2:000000000000:secret:placeholder' }} - DOCKER_DIGEST: ${{ needs.prepare-webapp.outputs.DOCKER_DIGEST }} - REPLACE_DB_CLUSTER: ${{ inputs.deployment_type == 'Deployment with DB recreation' }} + needs: build-and-push-image permissions: id-token: write steps: - name: Checkout code uses: actions/checkout@v5 + with: + ref: ${{ inputs.git_sha_to_deploy || github.sha }} - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: - role-to-assume: ${{ env.aws_role }} + role-to-assume: ${{ env.aws-role }} aws-region: eu-west-2 - - name: Install terraform - uses: hashicorp/setup-terraform@v3 + - name: Setup python + uses: actions/setup-python@v4 with: - terraform_version: 1.11.4 - - name: Get db secret arn - id: get-db-secret-arn - working-directory: terraform/app + python-version: 3.12.3 + cache: pip + - name: Install Python dependencies + run: python3 -m pip install -r script/requirements.txt + - name: Get image digest + id: get-image-digest run: | - terraform init -backend-config="env/${{ inputs.environment }}-backend.hcl" -upgrade - DB_SECRET_ARN=$(terraform output --raw db_secret_arn) - echo "DB_SECRET_ARN=$DB_SECRET_ARN" >> $GITHUB_OUTPUT - - name: Terraform Plan - id: plan + digest=$(aws ecr describe-images \ + --repository-name mavis/webapp \ + --image-ids imageTag=${{ inputs.git_sha_to_deploy || github.sha }} \ + --query 'imageDetails[0].imageDigest' \ + --output text) + echo "digest=$digest" >> $GITHUB_OUTPUT + - name: Parse environment variables + id: parse-environment-variables run: | - set -eo pipefail - terraform init -backend-config="env/${{ inputs.environment }}-backend.hcl" -upgrade - - CIDR_BLOCKS='${{ inputs.egress_cidr }}' - PLAN_ARGS=( - "plan" - "-var=image_digest=${{ env.DOCKER_DIGEST }}" - "-var=db_secret_arn=${{ steps.get-db-secret-arn.outputs.DB_SECRET_ARN }}" - "-var=imported_snapshot=${{ env.SNAPSHOT_ARN }}" - "-var-file=env/${{ inputs.environment }}.tfvars" - "-var=allowed_egress_cidr_blocks=$CIDR_BLOCKS" - "-out=${{ runner.temp }}/tfplan" - ) - - if [ "${{ env.REPLACE_DB_CLUSTER }}" = "true" ]; then - PLAN_ARGS+=("-replace" "aws_rds_cluster.cluster") - fi - terraform "${PLAN_ARGS[@]}" | tee ${{ runner.temp }}/tf_stdout - - name: Upload artifact + parsed_env_vars=$(yq -r '.environments.${{ inputs.environment }} | to_entries | .[] | .key + "=" + .value' config/container_variables.yml) + { + echo 'parsed_env_vars<> "$GITHUB_OUTPUT" + - name: Populate web task definition + id: create-task-definition + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition-family: "mavis-data-replication-task-definition-${{ inputs.environment }}-template" + container-name: "application" + image: "${{ env.aws_account_id }}.dkr.ecr.eu-west-2.amazonaws.com/mavis/webapp@${{ steps.get-image-digest.outputs.digest }}" + environment-variables: ${{ steps.parse-environment-variables.outputs.parsed_env_vars }} + - name: Rename task definition file + run: mv ${{ steps.create-task-definition.outputs.task-definition }} ${{ runner.temp }}/data-replication-task-definition.json + - name: Upload artifact for data-replication task definition uses: actions/upload-artifact@v4 with: - name: tfplan_infrastructure-${{ inputs.environment }} - path: ${{ runner.temp }}/tfplan + name: ${{ inputs.environment }}-data-replication-task-definition + path: ${{ runner.temp }}/data-replication-task-definition.json - apply: - name: Terraform apply + approve-deployments: + name: Wait for approval if required runs-on: ubuntu-latest - needs: plan - if: ${{ !cancelled() && needs.plan.result == 'success' }} + needs: prepare-deployment environment: ${{ inputs.environment }} + steps: + - run: echo "Proceeding with deployment to ${{ inputs.environment }} environment" + + deploy-data-replication: + name: Deploy data-replication service + runs-on: ubuntu-latest + needs: [ prepare-deployment, approve-deployments ] permissions: id-token: write steps: - - name: Checkout code - uses: actions/checkout@v5 - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: - role-to-assume: ${{ env.aws_role }} + role-to-assume: ${{ env.aws-role }} aws-region: eu-west-2 - - name: Download artifact + - name: Download data-replication task definition artifact uses: actions/download-artifact@v5 with: - name: tfplan_infrastructure-${{ inputs.environment }} path: ${{ runner.temp }} - - name: Install terraform - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: 1.11.4 - - name: Apply the changes + name: ${{ inputs.environment }}-data-replication-task-definition + - name: Change family of task definition run: | - set -e - terraform init -backend-config="env/${{ inputs.environment }}-backend.hcl" -upgrade - terraform apply ${{ runner.temp }}/tfplan - - name: Deploy db-access-service - run: | - task_definition_arn=$(terraform output -raw task_definition_arn) - aws ecs update-service \ - --cluster mavis-${{ inputs.environment }}-data-replication \ - --service mavis-${{ inputs.environment }}-data-replication \ - --task-definition $task_definition_arn + file_path="${{ runner.temp }}/data-replication-task-definition.json" + family_name="mavis-data-replication-task-definition-${{ inputs.environment }}" + echo "$(jq --arg f "$family_name" '.family = $f' "$file_path")" > "$file_path" + - name: Deploy data-replication service + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: ${{ runner.temp }}/data-replication-task-definition.json + cluster: mavis-${{ inputs.environment }}-data-replication + service: mavis-${{ inputs.environment }}-data-replication + force-new-deployment: true + wait-for-service-stability: true \ No newline at end of file diff --git a/.github/workflows/refresh-data-replication.yml b/.github/workflows/refresh-data-replication.yml new file mode 100644 index 0000000000..2f0c866234 --- /dev/null +++ b/.github/workflows/refresh-data-replication.yml @@ -0,0 +1,190 @@ +name: Refresh Data Replication +run-name: Refresh data replication resources & data for ${{ inputs.environment }} + +on: + workflow_dispatch: + inputs: + environment: + description: Deployment environment + required: true + type: choice + options: + - training + - production + - test + - qa + - sandbox-alpha + - sandbox-beta + db_snapshot_arn: + description: ARN of the DB snapshot to use (optional) + required: false + type: string + egress_cidr: + description: CIDR blocks to allow egress traffic. + type: string + required: true + default: "[]" + take_db_snapshot: + description: Take a new DB snapshot before creating the environment + type: boolean + default: false + +permissions: {} + +env: + aws_role: ${{ inputs.environment == 'production' + && 'arn:aws:iam::820242920762:role/GithubDeployDataReplicationInfrastructure' + || 'arn:aws:iam::393416225559:role/GithubDeployDataReplicationInfrastructure' }} + db_snapshot_role: ${{ inputs.environment == 'production' + && 'arn:aws:iam::820242920762:role/DatabaseSnapshotRole' + || 'arn:aws:iam::393416225559:role/DatabaseSnapshotRole' }} + + +defaults: + run: + working-directory: terraform/data_replication + +concurrency: + group: deploy-data-replica-${{ inputs.environment }} + +jobs: + prepare-db-replica: + name: Prepare data replica + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@v5 + - name: Assume DB Snapshot role + if: inputs.take_db_snapshot + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.db_snapshot_role }} + aws-region: eu-west-2 + - name: Take DB snapshot + if: inputs.take_db_snapshot + run: | + set -e + snapshot_identifier=snapshot-for-data-replication-$(date +"%Y-%m-%d-%H-%M-%S") + aws rds create-db-cluster-snapshot --db-cluster-identifier mavis-${{ inputs.environment }} --db-cluster-snapshot-identifier $snapshot_identifier + echo "Waiting for snapshot to be available. This can take a while." + aws rds wait db-cluster-snapshot-available --db-cluster-snapshot-identifier $snapshot_identifier + echo "New snapshot is now available" + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.aws_role }} + aws-region: eu-west-2 + - name: get latest snapshot + id: get-latest-snapshot + run: | + set -e + if [ -z "${{ inputs.db_snapshot_arn }}" ]; then + echo "No snapshot ARN provided, fetching the latest snapshot" + SNAPSHOT_ARN=$(aws rds describe-db-cluster-snapshots \ + --query "DBClusterSnapshots[?DBClusterIdentifier=='mavis-${{ inputs.environment }}'].[DBClusterSnapshotArn, SnapshotCreateTime]" \ + --output text | sort -k2 -r | head -n 1 | cut -f1) + + if [ -z "$SNAPSHOT_ARN" ]; then + echo "No snapshots found for mavis-${{ inputs.environment }}" + exit 1 + fi + else + echo "Using provided snapshot ARN: ${{ inputs.db_snapshot_arn }}" + SNAPSHOT_ARN="${{ inputs.db_snapshot_arn }}" + fi + echo "Using snapshot ARN: $SNAPSHOT_ARN" + echo "SNAPSHOT_ARN=$SNAPSHOT_ARN" >> $GITHUB_OUTPUT + - name: Install terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.11.4 + outputs: + SNAPSHOT_ARN: ${{ steps.get-latest-snapshot.outputs.SNAPSHOT_ARN }} + + plan: + name: Terraform plan + runs-on: ubuntu-latest + needs: + - prepare-db-replica + env: + SNAPSHOT_ARN: ${{ needs.prepare-db-replica.outputs.SNAPSHOT_ARN }} + DB_SECRET_ARN: ${{ needs.prepare-db-replica.outputs.DB_SECRET_ARN || 'arn:aws:secretsmanager:eu-west-2:000000000000:secret:placeholder' }} + REPLACE_DB_CLUSTER: ${{ inputs.deployment_type == 'Deployment with DB recreation' }} + permissions: + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@v5 + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.aws_role }} + aws-region: eu-west-2 + - name: Install terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.11.4 + - name: Get db secret arn + id: get-db-secret-arn + working-directory: terraform/app + run: | + terraform init -backend-config="env/${{ inputs.environment }}-backend.hcl" -upgrade + DB_SECRET_ARN=$(terraform output --raw db_secret_arn) + echo "DB_SECRET_ARN=$DB_SECRET_ARN" >> $GITHUB_OUTPUT + - name: Terraform Plan + id: plan + run: | + set -eo pipefail + terraform init -backend-config="env/${{ inputs.environment }}-backend.hcl" -upgrade + + CIDR_BLOCKS='${{ inputs.egress_cidr }}' + PLAN_ARGS=( + "plan" + "-var=db_secret_arn=${{ steps.get-db-secret-arn.outputs.DB_SECRET_ARN }}" + "-var=imported_snapshot=${{ env.SNAPSHOT_ARN }}" + "-var-file=env/${{ inputs.environment }}.tfvars" + "-var=allowed_egress_cidr_blocks=$CIDR_BLOCKS" + "-out=${{ runner.temp }}/tfplan" + ) + + if [ "${{ env.REPLACE_DB_CLUSTER }}" = "true" ]; then + PLAN_ARGS+=("-replace" "aws_rds_cluster.cluster") + fi + terraform "${PLAN_ARGS[@]}" | tee ${{ runner.temp }}/tf_stdout + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: tfplan_infrastructure-${{ inputs.environment }} + path: ${{ runner.temp }}/tfplan + + apply: + name: Terraform apply + runs-on: ubuntu-latest + needs: plan + environment: ${{ inputs.environment }} + permissions: + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@v5 + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.aws_role }} + aws-region: eu-west-2 + - name: Download artifact + uses: actions/download-artifact@v5 + with: + name: tfplan_infrastructure-${{ inputs.environment }} + path: ${{ runner.temp }} + - name: Install terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.11.4 + - name: Apply the changes + run: | + set -e + terraform init -backend-config="env/${{ inputs.environment }}-backend.hcl" -upgrade + terraform apply ${{ runner.temp }}/tfplan From 01d32d1a198132978550b89fda39c14f4882fdcb Mon Sep 17 00:00:00 2001 From: Brage Gording Date: Thu, 28 Aug 2025 14:01:53 +0100 Subject: [PATCH 06/18] Clean up empty files - Remnant from database migration --- terraform/app/db_migration_configuration.tf | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 terraform/app/db_migration_configuration.tf diff --git a/terraform/app/db_migration_configuration.tf b/terraform/app/db_migration_configuration.tf deleted file mode 100644 index e69de29bb2..0000000000 From 629a650a9eb9a0fc40051d3111b81181330ba588 Mon Sep 17 00:00:00 2001 From: Brage Gording Date: Tue, 2 Sep 2025 10:16:27 +0100 Subject: [PATCH 07/18] Standardizing conventions/setup - Dashes instead of underline for github pipelines - Clear separation of concerns in account tf stack - Remove unnecessary `app_version` input --- .../workflows/data-replication-pipeline.yml | 8 +-- .github/workflows/deploy-application.yml | 55 +++++++++---------- .github/workflows/deploy-infrastructure.yml | 18 +++--- .github/workflows/deploy.yml | 33 ++++++----- .../workflows/refresh-data-replication.yml | 1 + .tool-versions | 2 +- terraform/account/deployment_permissions.tf | 2 + 7 files changed, 57 insertions(+), 62 deletions(-) diff --git a/.github/workflows/data-replication-pipeline.yml b/.github/workflows/data-replication-pipeline.yml index 113f6e1c41..5f86aa239a 100644 --- a/.github/workflows/data-replication-pipeline.yml +++ b/.github/workflows/data-replication-pipeline.yml @@ -23,7 +23,7 @@ on: permissions: {} env: - aws-role: ${{ inputs.environment == 'production' + aws_role: ${{ inputs.environment == 'production' && 'arn:aws:iam::820242920762:role/GithubDeployDataReplicationInfrastructure' || 'arn:aws:iam::393416225559:role/GithubDeployDataReplicationInfrastructure' }} aws_account_id: ${{ inputs.environment == 'production' && '820242920762' || '393416225559' }} @@ -80,15 +80,13 @@ jobs: - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: - role-to-assume: ${{ env.aws-role }} + role-to-assume: ${{ env.aws_role }} aws-region: eu-west-2 - name: Setup python uses: actions/setup-python@v4 with: python-version: 3.12.3 cache: pip - - name: Install Python dependencies - run: python3 -m pip install -r script/requirements.txt - name: Get image digest id: get-image-digest run: | @@ -145,7 +143,7 @@ jobs: - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: - role-to-assume: ${{ env.aws-role }} + role-to-assume: ${{ env.aws_role }} aws-region: eu-west-2 - name: Download data-replication task definition artifact uses: actions/download-artifact@v5 diff --git a/.github/workflows/deploy-application.yml b/.github/workflows/deploy-application.yml index 6896a92dd7..a7d15af7f3 100644 --- a/.github/workflows/deploy-application.yml +++ b/.github/workflows/deploy-application.yml @@ -16,7 +16,7 @@ on: - production - sandbox-alpha - sandbox-beta - server_types: + server-types: description: Server types to deploy required: true type: choice @@ -26,7 +26,7 @@ on: - good-job - sidekiq default: all - git_sha_to_deploy: + git-ref-to-deploy: description: The git commit SHA to deploy. required: false type: string @@ -35,17 +35,13 @@ on: environment: required: true type: string - server_types: + server-types: required: true type: string - git_sha_to_deploy: + git-ref-to-deploy: description: The git commit SHA to deploy. required: true type: string - app_version: - description: The git ref to deploy (branch, tag, or commit SHA). - required: false - type: string permissions: {} @@ -56,9 +52,9 @@ env: aws-role: ${{ inputs.environment == 'production' && 'arn:aws:iam::820242920762:role/GithubDeployECSService' || 'arn:aws:iam::393416225559:role/GithubDeployECSService' }} - aws_account_id: ${{ inputs.environment == 'production' && '820242920762' || '393416225559' }} - cluster_name: mavis-${{ inputs.environment }} - app_version: ${{ inputs.app_version == '' && 'unknown' || inputs.app_version }} + aws-account-id: ${{ inputs.environment == 'production' && '820242920762' || '393416225559' }} + cluster-name: mavis-${{ inputs.environment }} + app-version: ${{ inputs.git-ref-to-deploy == '' && 'unknown' || inputs.git-ref-to-deploy }} jobs: prepare-deployment: @@ -69,12 +65,13 @@ jobs: strategy: fail-fast: true matrix: - service: ${{ inputs.server_types == 'all' && fromJSON('["web", "good-job", "sidekiq"]') || fromJSON(format('["{0}"]', inputs.server_types)) }} + service: ${{ inputs.server-types == 'all' && fromJSON('["web", "good-job", "sidekiq"]') || fromJSON(format('["{0}"]', inputs.server-types)) }} steps: - name: Checkout code uses: actions/checkout@v5 + id: checkout-code with: - ref: ${{ inputs.git_sha_to_deploy || github.sha }} + ref: ${{ inputs.git-ref-to-deploy || github.sha }} - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: @@ -83,7 +80,7 @@ jobs: - name: Setup python uses: actions/setup-python@v4 with: - python-version: 3.12.3 + python-version: 3.13.7 cache: pip - name: Install Python dependencies run: python3 -m pip install -r script/requirements.txt @@ -92,7 +89,7 @@ jobs: run: | digest=$(aws ecr describe-images \ --repository-name mavis/webapp \ - --image-ids imageTag=${{ inputs.git_sha_to_deploy || github.sha }} \ + --image-ids imageTag=${{ steps.checkout-code.outputs.commit }} \ --query 'imageDetails[0].imageDigest' \ --output text) echo "digest=$digest" >> $GITHUB_OUTPUT @@ -111,13 +108,13 @@ jobs: with: task-definition-family: "mavis-${{ matrix.service }}-task-definition-${{ inputs.environment }}-template" container-name: "application" - image: "${{ env.aws_account_id }}.dkr.ecr.eu-west-2.amazonaws.com/mavis/webapp@${{ steps.get-image-digest.outputs.digest }}" + image: "${{ env.aws-account-id }}.dkr.ecr.eu-west-2.amazonaws.com/mavis/webapp@${{ steps.get-image-digest.outputs.digest }}" environment-variables: ${{ steps.parse-environment-variables.outputs.parsed_env_vars }} - name: Rename task definition file run: mv ${{ steps.create-task-definition.outputs.task-definition }} ${{ runner.temp }}/${{ matrix.service }}-task-definition.json - name: Populate SSM parameters for ${{ matrix.service }} service run: | - python3 script/populate_ssm_parameters.py ${{ inputs.environment }} ${{ matrix.service }} --app-version ${{ env.app_version }} + python3 script/populate_ssm_parameters.py ${{ inputs.environment }} ${{ matrix.service }} --app-version ${{ env.app-version }} - name: Upload artifact for ${{ matrix.service }} task definition uses: actions/upload-artifact@v4 with: @@ -135,8 +132,8 @@ jobs: deploy-web: name: Deploy web service runs-on: ubuntu-latest - if: ${{ inputs.server_types == 'web' || inputs.server_types == 'all' }} - needs: [prepare-deployment, approve-deployments] + if: ${{ inputs.server-types == 'web' || inputs.server-types == 'all' }} + needs: [ prepare-deployment, approve-deployments ] permissions: id-token: write steps: @@ -148,7 +145,7 @@ jobs: - name: Checkout code uses: actions/checkout@v5 - name: Download web task definition artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: path: ${{ runner.temp }} name: ${{ inputs.environment }}-web-task-definition @@ -167,7 +164,7 @@ jobs: with: task-definition: ${{ runner.temp }}/web-task-definition.json codedeploy-appspec: config/templates/appspec.yaml - cluster: ${{ env.cluster_name }} + cluster: ${{ env.cluster-name }} service: mavis-${{ inputs.environment }}-web codedeploy-application: mavis-${{ inputs.environment }} codedeploy-deployment-group: blue-green-group-${{ inputs.environment }} @@ -180,13 +177,13 @@ jobs: deploy-good-job: name: Deploy good-job service runs-on: ubuntu-latest - if: ${{ inputs.server_types == 'good-job' || inputs.server_types == 'all' }} - needs: [prepare-deployment, approve-deployments] + if: ${{ inputs.server-types == 'good-job' || inputs.server-types == 'all' }} + needs: [ prepare-deployment, approve-deployments ] permissions: id-token: write steps: - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v5 with: role-to-assume: ${{ env.aws-role }} aws-region: eu-west-2 @@ -204,7 +201,7 @@ jobs: uses: aws-actions/amazon-ecs-deploy-task-definition@v2 with: task-definition: ${{ runner.temp }}/good-job-task-definition.json - cluster: ${{ env.cluster_name }} + cluster: ${{ env.cluster-name }} service: mavis-${{ inputs.environment }}-good-job force-new-deployment: true wait-for-service-stability: true @@ -212,8 +209,8 @@ jobs: create-sidekiq-deployment: name: Create sidekiq deployment runs-on: ubuntu-latest - if: ${{ inputs.server_types == 'sidekiq' || inputs.server_types == 'all' }} - needs: [prepare-deployment, approve-deployments] + if: ${{ inputs.server-types == 'sidekiq' || inputs.server-types == 'all' }} + needs: [ prepare-deployment, approve-deployments ] permissions: id-token: write steps: @@ -223,7 +220,7 @@ jobs: role-to-assume: ${{ env.aws-role }} aws-region: eu-west-2 - name: Download sidekiq task definition artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: path: ${{ runner.temp }} name: ${{ inputs.environment }}-sidekiq-task-definition @@ -236,7 +233,7 @@ jobs: uses: aws-actions/amazon-ecs-deploy-task-definition@v2 with: task-definition: ${{ runner.temp }}/sidekiq-task-definition.json - cluster: ${{ env.cluster_name }} + cluster: ${{ env.cluster-name }} service: mavis-${{ inputs.environment }}-sidekiq force-new-deployment: true wait-for-service-stability: true diff --git a/.github/workflows/deploy-infrastructure.yml b/.github/workflows/deploy-infrastructure.yml index 99d2491f41..e273a51338 100644 --- a/.github/workflows/deploy-infrastructure.yml +++ b/.github/workflows/deploy-infrastructure.yml @@ -8,7 +8,7 @@ on: description: Deployment environment required: true type: string - git_ref_to_deploy: + git-ref-to-deploy: required: true type: string workflow_dispatch: @@ -17,7 +17,7 @@ on: description: Deployment environment required: true type: string - git_ref_to_deploy: + git-ref-to-deploy: description: The git commit SHA to deploy. required: false type: string @@ -28,12 +28,10 @@ concurrency: group: deploy-infrastructure-${{ inputs.environment }} env: - aws_role: ${{ inputs.environment == 'production' + aws-role: ${{ inputs.environment == 'production' && 'arn:aws:iam::820242920762:role/GithubDeployMavisAndInfrastructure' || 'arn:aws:iam::393416225559:role/GithubDeployMavisAndInfrastructure' }} - aws_account_id: ${{ inputs.environment == 'production' - && '820242920762' || '393416225559' }} - git_ref_to_deploy: ${{ inputs.git_ref_to_deploy || github.ref_name }} + git-ref-to-deploy: ${{ inputs.git-ref-to-deploy || github.ref_name }} defaults: run: @@ -49,11 +47,11 @@ jobs: - name: Checkout code uses: actions/checkout@v5 with: - ref: ${{ env.git_ref_to_deploy }} + ref: ${{ env.git-ref-to-deploy }} - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: - role-to-assume: ${{ env.aws_role }} + role-to-assume: ${{ env.aws-role }} aws-region: eu-west-2 - name: Install terraform uses: hashicorp/setup-terraform@v3 @@ -92,11 +90,11 @@ jobs: - name: Checkout code uses: actions/checkout@v5 with: - ref: ${{ env.git_ref_to_deploy }} + ref: ${{ env.git-ref-to-deploy }} - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: - role-to-assume: ${{ env.aws_role }} + role-to-assume: ${{ env.aws-role }} aws-region: eu-west-2 - name: Download artifact uses: actions/download-artifact@v5 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bef703aede..bd44f2c4ec 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,5 +1,5 @@ name: Deploy -run-name: Deploy ${{ inputs.git_ref_to_deploy || github.ref_name }} to ${{ inputs.environment }} +run-name: Deploy ${{ inputs.git-ref-to-deploy || github.ref_name }} to ${{ inputs.environment }} concurrency: group: deploy-${{ inputs.environment }} @@ -10,12 +10,12 @@ on: environment: required: true type: string - server_types: + server-types: required: true type: string workflow_dispatch: inputs: - git_ref_to_deploy: + git-ref-to-deploy: description: | # Use blank unicode character (U+2800) to force line-break Use code from: ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ @@ -33,7 +33,7 @@ on: - production - sandbox-alpha - sandbox-beta - server_types: + server-types: description: Server types to deploy required: true type: choice @@ -46,7 +46,7 @@ on: default: all env: - account_id: ${{ inputs.environment == 'production' && '820242920762' || '393416225559' }} + account-id: ${{ inputs.environment == 'production' && '820242920762' || '393416225559' }} jobs: validate-inputs: @@ -56,8 +56,8 @@ jobs: - name: Validate inputs run: | if [[ "${{ inputs.environment }}" == "preview" || "${{ inputs.environment }}" == "production" ]]; then - if [[ -z "${{ inputs.git_ref_to_deploy }}" ]]; then - echo "Error: git_ref_to_deploy is required for preview and production environments." + if [[ -z "${{ inputs.git-ref-to-deploy }}" ]]; then + echo "Error: git-ref-to-deploy is required for preview and production environments." exit 1 fi fi @@ -71,7 +71,7 @@ jobs: - name: Checkout code uses: actions/checkout@v5 with: - ref: ${{ inputs.git_ref_to_deploy || github.sha }} + ref: ${{ inputs.git-ref-to-deploy || github.sha }} - name: Get git sha id: get-git-sha run: echo "git-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT @@ -98,13 +98,13 @@ jobs: - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: - role-to-assume: arn:aws:iam::${{ env.account_id }}:role/GithubDeployMavisAndInfrastructure + role-to-assume: arn:aws:iam::${{ env.account-id }}:role/GithubDeployMavisAndInfrastructure aws-region: eu-west-2 - name: Compare permissions id: compare-permissions run: | source ./scripts/validate-github-actions-policy.sh - validate_policies arn:aws:iam::${{ env.account_id }}:policy/DeployMavisResources ./account/resources/iam_policy_DeployMavisResources.json + validate_policies arn:aws:iam::${{ env.account-id }}:policy/DeployMavisResources ./account/resources/iam_policy_DeployMavisResources.json exit $? update-permissions: runs-on: ubuntu-latest @@ -124,10 +124,10 @@ jobs: - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: - role-to-assume: arn:aws:iam::${{ env.account_id }}:role/GithubDeployMavisAndInfrastructure + role-to-assume: arn:aws:iam::${{ env.account-id }}:role/GithubDeployMavisAndInfrastructure aws-region: eu-west-2 - name: Update IAM policy - run: ./scripts/update-github-actions-policy.sh arn:aws:iam::${{ env.account_id }}:policy/DeployMavisResources ./account/resources/iam_policy_DeployMavisResources.json + run: ./scripts/update-github-actions-policy.sh arn:aws:iam::${{ env.account-id }}:policy/DeployMavisResources ./account/resources/iam_policy_DeployMavisResources.json deploy-infrastructure: permissions: id-token: write @@ -144,15 +144,14 @@ jobs: uses: ./.github/workflows/deploy-infrastructure.yml with: environment: ${{ inputs.environment }} - git_ref_to_deploy: ${{ inputs.git_ref_to_deploy || github.ref_name }} + git-ref-to-deploy: ${{ inputs.git-ref-to-deploy || github.ref_name }} deploy-application: permissions: id-token: write needs: [deploy-infrastructure, determine-git-sha] - if: ${{ !cancelled() && inputs.server_types != 'none' && needs.deploy-infrastructure.result == 'success' }} + if: ${{ !cancelled() && inputs.server-types != 'none' && needs.deploy-infrastructure.result == 'success' }} uses: ./.github/workflows/deploy-application.yml with: environment: ${{ inputs.environment }} - server_types: ${{ inputs.server_types }} - git_sha_to_deploy: ${{ needs.determine-git-sha.outputs.git-sha }} - app_version: ${{ inputs.git_ref_to_deploy }} + server-types: ${{ inputs.server-types }} + git-ref-to-deploy: ${{ inputs.git-ref-to-deploy || github.ref_name }} diff --git a/.github/workflows/refresh-data-replication.yml b/.github/workflows/refresh-data-replication.yml index 2f0c866234..f8b8fc48b2 100644 --- a/.github/workflows/refresh-data-replication.yml +++ b/.github/workflows/refresh-data-replication.yml @@ -147,6 +147,7 @@ jobs: "-var-file=env/${{ inputs.environment }}.tfvars" "-var=allowed_egress_cidr_blocks=$CIDR_BLOCKS" "-out=${{ runner.temp }}/tfplan" + "-replace" "aws_rds_cluster.cluster" ) if [ "${{ env.REPLACE_DB_CLUSTER }}" = "true" ]; then diff --git a/.tool-versions b/.tool-versions index add02938b1..397eb65317 100644 --- a/.tool-versions +++ b/.tool-versions @@ -3,7 +3,7 @@ hk 1.1.2 nodejs 22.15.0 pkl 0.28.1 postgres 17.2 -python 3.12.3 +python 3.13.7 redis 8.2.1 ruby 3.4.3 terraform 1.11.4 diff --git a/terraform/account/deployment_permissions.tf b/terraform/account/deployment_permissions.tf index 271af618fe..c9212310a8 100644 --- a/terraform/account/deployment_permissions.tf +++ b/terraform/account/deployment_permissions.tf @@ -48,6 +48,8 @@ resource "aws_iam_role_policy_attachment" "data_replication" { policy_arn = each.value } +################# DB Snapshot Policy ################ + resource "aws_iam_role" "data_replication_snapshot" { name = "DatabaseSnapshotRole" description = "Role to be assumed by the data replication workflow for taking on-demand DB snapshots" From d24dabbca9bb2ebd50eb98964dae79c14a1a22a1 Mon Sep 17 00:00:00 2001 From: Brage Gording Date: Tue, 2 Sep 2025 12:29:11 +0100 Subject: [PATCH 08/18] Move export of ENV_VARS - Docker entry point is top level so this is where the environment variables must be extracted - Otherwise shelling into the container and also db:seed does not execute properly --- bin/docker-entrypoint | 12 ++++++++++++ bin/docker-start | 11 ----------- script/shell.sh | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index c9330c2273..88361ba027 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -6,6 +6,18 @@ if [ -z "${LD_PRELOAD+x}" ]; then export LD_PRELOAD fi + +# Extract ENV_VARS into set of variables and export them to the shell +if [ -n "$ENV_VARS" ]; then + IFS=',' read -ra ADDR <<< "$ENV_VARS" + for i in "${ADDR[@]}"; do + if [[ "$i" == *"="* ]]; then + export "${i?}" + echo "Exported: $i" + fi + done +fi + # When starting the container then create or migrate existing database if [ "$1" == "./bin/docker-start" ]; then ./bin/rails db:prepare:ignore_concurrent_migration_exceptions diff --git a/bin/docker-start b/bin/docker-start index 53d19bdcc0..17fa5448d7 100755 --- a/bin/docker-start +++ b/bin/docker-start @@ -2,17 +2,6 @@ BIN_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -# Extract ENV_VARS into set of variables and export them to the shell -if [ -n "$ENV_VARS" ]; then - IFS=',' read -ra ADDR <<< "$ENV_VARS" - for i in "${ADDR[@]}"; do - if [[ "$i" == *"="* ]]; then - export "${i?}" - echo "Exported: $i" - fi - done -fi - if [ "$SERVER_TYPE" == "web" ]; then echo "Starting web server..." exec "$BIN_DIR"/thrust "$BIN_DIR"/rails server diff --git a/script/shell.sh b/script/shell.sh index 0a52d3046e..db9e512260 100755 --- a/script/shell.sh +++ b/script/shell.sh @@ -161,5 +161,5 @@ aws ecs execute-command --region "$region" \ --cluster "$cluster_name" \ --task "$task_id" \ --container "$container_name" \ - --command "/bin/bash" \ + --command "/rails/bin/docker-entrypoint /bin/bash" \ --interactive From e1f805315efb3093dce8d28082ca730c5985468e Mon Sep 17 00:00:00 2001 From: Theodor Vararu Date: Thu, 11 Sep 2025 16:07:28 +0700 Subject: [PATCH 09/18] Fix missing newlines from infrastructure files Also format some yml whitespace using prettier. --- .github/workflows/data-replication-pipeline.yml | 8 ++++---- script/populate_ssm_parameters.py | 14 +++++++------- script/requirements.txt | 2 +- terraform/account/deployment_permissions.tf | 2 +- terraform/app/modules/ecs_service/main.tf | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/data-replication-pipeline.yml b/.github/workflows/data-replication-pipeline.yml index 5f86aa239a..af2a1e2cf6 100644 --- a/.github/workflows/data-replication-pipeline.yml +++ b/.github/workflows/data-replication-pipeline.yml @@ -34,7 +34,7 @@ concurrency: jobs: validate-inputs: runs-on: ubuntu-latest - permissions: { } + permissions: {} steps: - name: Validate inputs run: | @@ -46,7 +46,7 @@ jobs: fi determine-git-sha: runs-on: ubuntu-latest - permissions: { } + permissions: {} needs: validate-inputs outputs: git-sha: ${{ steps.get-git-sha.outputs.git-sha }} @@ -136,7 +136,7 @@ jobs: deploy-data-replication: name: Deploy data-replication service runs-on: ubuntu-latest - needs: [ prepare-deployment, approve-deployments ] + needs: [prepare-deployment, approve-deployments] permissions: id-token: write steps: @@ -162,4 +162,4 @@ jobs: cluster: mavis-${{ inputs.environment }}-data-replication service: mavis-${{ inputs.environment }}-data-replication force-new-deployment: true - wait-for-service-stability: true \ No newline at end of file + wait-for-service-stability: true diff --git a/script/populate_ssm_parameters.py b/script/populate_ssm_parameters.py index a0e2d996cb..41c6339395 100755 --- a/script/populate_ssm_parameters.py +++ b/script/populate_ssm_parameters.py @@ -24,12 +24,12 @@ def load_yaml_config(file_path: str) -> Dict[str, Any]: def extract_cloud_variables(config: Dict[str, Any], environment: str, server_type: str) -> List[str]: """Extract cloud variables from YAML config for the given environment and server type.""" cloud_vars = [] - + env_config = config[environment] if server_type in env_config: for key, value in env_config[server_type].items(): cloud_vars.append(f"{key}={value}") - + return cloud_vars @@ -39,16 +39,16 @@ def update_ssm_parameter(parameter_name: str, values: List[str], app_version: st ssm = boto3.client('ssm') values.append(f"app_version={app_version}") string_list = ','.join(values) - + print(f"Updating SSM parameter: {parameter_name}") - + ssm.put_parameter( Name=parameter_name, Value=string_list, Type='StringList', Overwrite=True ) - + except Exception as e: print(f"Error: Failed to update SSM parameter '{parameter_name}': {e}") sys.exit(1) @@ -67,7 +67,7 @@ def main(): parser.add_argument('environment', help='Environment name (e.g., qa, production, etc.)') parser.add_argument('server_type', help='Server type') - parser.add_argument('-c', '--config-file', default='config/container_variables.yml', + parser.add_argument('-c', '--config-file', default='config/container_variables.yml', help='Container variables file path (default: config/container_variables.yml)') parser.add_argument('--app-version', default='unknown', help='Application version (default: unknown)') @@ -87,4 +87,4 @@ def main(): print(f"Cloud variables updated successfully") if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/script/requirements.txt b/script/requirements.txt index 57ceb3f754..b6119c043c 100644 --- a/script/requirements.txt +++ b/script/requirements.txt @@ -1,2 +1,2 @@ boto3>=1.39.0 -PyYAML>=6.0 \ No newline at end of file +PyYAML>=6.0 diff --git a/terraform/account/deployment_permissions.tf b/terraform/account/deployment_permissions.tf index c9212310a8..9ff3877780 100644 --- a/terraform/account/deployment_permissions.tf +++ b/terraform/account/deployment_permissions.tf @@ -137,4 +137,4 @@ resource "aws_iam_role_policy_attachment" "deploy_ecs_service" { for_each = local.ecs_deploy_policies role = aws_iam_role.deploy_ecs_service.name policy_arn = each.value -} \ No newline at end of file +} diff --git a/terraform/app/modules/ecs_service/main.tf b/terraform/app/modules/ecs_service/main.tf index 21e3f35ef3..68dce835b1 100644 --- a/terraform/app/modules/ecs_service/main.tf +++ b/terraform/app/modules/ecs_service/main.tf @@ -108,4 +108,4 @@ resource "aws_ecs_task_definition" "this" { } } ]) -} \ No newline at end of file +} From 6c61c57a62a9b73c656637e05556acf83a4057fb Mon Sep 17 00:00:00 2001 From: Theodor Vararu Date: Thu, 11 Sep 2025 16:08:09 +0700 Subject: [PATCH 10/18] Use latest python in data-replication-pipeline Matches the one in .tool-versions. --- .github/workflows/data-replication-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/data-replication-pipeline.yml b/.github/workflows/data-replication-pipeline.yml index af2a1e2cf6..3f16528f0d 100644 --- a/.github/workflows/data-replication-pipeline.yml +++ b/.github/workflows/data-replication-pipeline.yml @@ -85,7 +85,7 @@ jobs: - name: Setup python uses: actions/setup-python@v4 with: - python-version: 3.12.3 + python-version: 3.13.7 cache: pip - name: Get image digest id: get-image-digest From 93c3d523955f1d82dcf178fbc04120ade8471b55 Mon Sep 17 00:00:00 2001 From: Theodor Vararu Date: Thu, 11 Sep 2025 16:09:21 +0700 Subject: [PATCH 11/18] Remove unnecessary -replace check The workflow always replaces the db cluster, so the check isn't necessary anymore. --- .github/workflows/refresh-data-replication.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/refresh-data-replication.yml b/.github/workflows/refresh-data-replication.yml index f8b8fc48b2..be27c8d7da 100644 --- a/.github/workflows/refresh-data-replication.yml +++ b/.github/workflows/refresh-data-replication.yml @@ -39,7 +39,6 @@ env: && 'arn:aws:iam::820242920762:role/DatabaseSnapshotRole' || 'arn:aws:iam::393416225559:role/DatabaseSnapshotRole' }} - defaults: run: working-directory: terraform/data_replication @@ -85,7 +84,7 @@ jobs: SNAPSHOT_ARN=$(aws rds describe-db-cluster-snapshots \ --query "DBClusterSnapshots[?DBClusterIdentifier=='mavis-${{ inputs.environment }}'].[DBClusterSnapshotArn, SnapshotCreateTime]" \ --output text | sort -k2 -r | head -n 1 | cut -f1) - + if [ -z "$SNAPSHOT_ARN" ]; then echo "No snapshots found for mavis-${{ inputs.environment }}" exit 1 @@ -149,10 +148,7 @@ jobs: "-out=${{ runner.temp }}/tfplan" "-replace" "aws_rds_cluster.cluster" ) - - if [ "${{ env.REPLACE_DB_CLUSTER }}" = "true" ]; then - PLAN_ARGS+=("-replace" "aws_rds_cluster.cluster") - fi + terraform "${PLAN_ARGS[@]}" | tee ${{ runner.temp }}/tf_stdout - name: Upload artifact uses: actions/upload-artifact@v4 From d61156e9e3af84799a8ac27ef6dd4282f0540a0e Mon Sep 17 00:00:00 2001 From: Theodor Vararu Date: Thu, 11 Sep 2025 16:49:26 +0700 Subject: [PATCH 12/18] Tweak data-replication-pipeline Update the title, pass in `git_ref_to_deploy`, only `build-and-push-image` image if an `image_tag` isn't specified, and ensure that `yq` is installed before using it. --- .../workflows/data-replication-pipeline.yml | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/data-replication-pipeline.yml b/.github/workflows/data-replication-pipeline.yml index 3f16528f0d..cfd92cb3a7 100644 --- a/.github/workflows/data-replication-pipeline.yml +++ b/.github/workflows/data-replication-pipeline.yml @@ -1,5 +1,5 @@ -name: Deploy Data Replication -run-name: Deploy Data Replication service for ${{ inputs.environment }} +name: Deploy data replication +run-name: Deploy data replication to ${{ inputs.environment }} on: workflow_dispatch: @@ -16,7 +16,11 @@ on: - sandbox-alpha - sandbox-beta image_tag: - description: Docker image tag to deploy + description: Docker image tag to deploy (if not provided, will build from git_ref_to_deploy) + required: false + type: string + git_ref_to_deploy: + description: Git ref (branch/tag/SHA) to deploy required: false type: string @@ -39,11 +43,12 @@ jobs: - name: Validate inputs run: | if [[ "${{ inputs.environment }}" == "preview" || "${{ inputs.environment }}" == "production" ]]; then - if [[ -z "${{ inputs.git_ref_to_deploy }}" ]]; then - echo "Error: git_ref_to_deploy is required for preview and production environments." + if [[ -z "${{ inputs.git_ref_to_deploy }}" && -z "${{ inputs.image_tag }}" ]]; then + echo "Error: Either git_ref_to_deploy or image_tag is required for preview and production environment." exit 1 fi fi + determine-git-sha: runs-on: ubuntu-latest permissions: {} @@ -58,7 +63,9 @@ jobs: - name: Get git sha id: get-git-sha run: echo "git-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + build-and-push-image: + if: ${{ !inputs.image_tag }} permissions: id-token: write needs: determine-git-sha @@ -69,14 +76,15 @@ jobs: prepare-deployment: name: Prepare deployment runs-on: ubuntu-latest - needs: build-and-push-image + needs: [determine-git-sha, build-and-push-image] + if: ${{ always() && (needs.build-and-push-image.result == 'success' || needs.build-and-push-image.result == 'skipped') }} permissions: id-token: write steps: - name: Checkout code uses: actions/checkout@v5 with: - ref: ${{ inputs.git_sha_to_deploy || github.sha }} + ref: ${{ inputs.git_ref_to_deploy || github.sha }} - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: @@ -87,12 +95,14 @@ jobs: with: python-version: 3.13.7 cache: pip + - name: Install yq + run: pip install yq - name: Get image digest id: get-image-digest run: | digest=$(aws ecr describe-images \ --repository-name mavis/webapp \ - --image-ids imageTag=${{ inputs.git_sha_to_deploy || github.sha }} \ + --image-ids imageTag=${{ inputs.image_tag || needs.determine-git-sha.outputs.git-sha }} \ --query 'imageDetails[0].imageDigest' \ --output text) echo "digest=$digest" >> $GITHUB_OUTPUT From cba341d6a8d35af9457b13238783b8f6932fe0e2 Mon Sep 17 00:00:00 2001 From: Theodor Vararu Date: Thu, 11 Sep 2025 16:53:17 +0700 Subject: [PATCH 13/18] Use dashes for inputs to data-replication-pipeline Matches the convention that GitHub actions generally use. --- .../workflows/data-replication-pipeline.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/data-replication-pipeline.yml b/.github/workflows/data-replication-pipeline.yml index cfd92cb3a7..02895d04a1 100644 --- a/.github/workflows/data-replication-pipeline.yml +++ b/.github/workflows/data-replication-pipeline.yml @@ -15,11 +15,11 @@ on: - qa - sandbox-alpha - sandbox-beta - image_tag: - description: Docker image tag to deploy (if not provided, will build from git_ref_to_deploy) + image-tag: + description: Docker image tag to deploy (if not provided, will build from git-ref-to-deploy) required: false type: string - git_ref_to_deploy: + git-ref-to-deploy: description: Git ref (branch/tag/SHA) to deploy required: false type: string @@ -43,8 +43,8 @@ jobs: - name: Validate inputs run: | if [[ "${{ inputs.environment }}" == "preview" || "${{ inputs.environment }}" == "production" ]]; then - if [[ -z "${{ inputs.git_ref_to_deploy }}" && -z "${{ inputs.image_tag }}" ]]; then - echo "Error: Either git_ref_to_deploy or image_tag is required for preview and production environment." + if [[ -z "${{ inputs.git-ref-to-deploy }}" && -z "${{ inputs.image-tag }}" ]]; then + echo "Error: Either git-ref-to-deploy or image-tag is required for preview and production environment." exit 1 fi fi @@ -59,13 +59,13 @@ jobs: - name: Checkout code uses: actions/checkout@v5 with: - ref: ${{ inputs.git_ref_to_deploy || github.sha }} + ref: ${{ inputs.git-ref-to-deploy || github.sha }} - name: Get git sha id: get-git-sha run: echo "git-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT build-and-push-image: - if: ${{ !inputs.image_tag }} + if: ${{ !inputs.image-tag }} permissions: id-token: write needs: determine-git-sha @@ -84,7 +84,7 @@ jobs: - name: Checkout code uses: actions/checkout@v5 with: - ref: ${{ inputs.git_ref_to_deploy || github.sha }} + ref: ${{ inputs.git-ref-to-deploy || github.sha }} - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v5 with: @@ -102,7 +102,7 @@ jobs: run: | digest=$(aws ecr describe-images \ --repository-name mavis/webapp \ - --image-ids imageTag=${{ inputs.image_tag || needs.determine-git-sha.outputs.git-sha }} \ + --image-ids imageTag=${{ inputs.image-tag || needs.determine-git-sha.outputs.git-sha }} \ --query 'imageDetails[0].imageDigest' \ --output text) echo "digest=$digest" >> $GITHUB_OUTPUT From 8d7266225c057fa8bd0f956923e419c584f39133 Mon Sep 17 00:00:00 2001 From: Theodor Vararu Date: Thu, 11 Sep 2025 16:56:20 +0700 Subject: [PATCH 14/18] Remove good-job deployment from deploy-application We're now fully relying on sidekiq, so don't need this anymore. --- .github/workflows/deploy-application.yml | 39 ++---------------------- 1 file changed, 3 insertions(+), 36 deletions(-) diff --git a/.github/workflows/deploy-application.yml b/.github/workflows/deploy-application.yml index a7d15af7f3..a701ebbf89 100644 --- a/.github/workflows/deploy-application.yml +++ b/.github/workflows/deploy-application.yml @@ -23,7 +23,6 @@ on: options: - all - web - - good-job - sidekiq default: all git-ref-to-deploy: @@ -65,7 +64,7 @@ jobs: strategy: fail-fast: true matrix: - service: ${{ inputs.server-types == 'all' && fromJSON('["web", "good-job", "sidekiq"]') || fromJSON(format('["{0}"]', inputs.server-types)) }} + service: ${{ inputs.server-types == 'all' && fromJSON('["web", "sidekiq"]') || fromJSON(format('["{0}"]', inputs.server-types)) }} steps: - name: Checkout code uses: actions/checkout@v5 @@ -133,7 +132,7 @@ jobs: name: Deploy web service runs-on: ubuntu-latest if: ${{ inputs.server-types == 'web' || inputs.server-types == 'all' }} - needs: [ prepare-deployment, approve-deployments ] + needs: [prepare-deployment, approve-deployments] permissions: id-token: write steps: @@ -174,43 +173,11 @@ jobs: aws deploy wait deployment-successful --deployment-id "${{ steps.deploy-web-service.outputs.codedeploy-deployment-id }}" echo "Deployment successful" - deploy-good-job: - name: Deploy good-job service - runs-on: ubuntu-latest - if: ${{ inputs.server-types == 'good-job' || inputs.server-types == 'all' }} - needs: [ prepare-deployment, approve-deployments ] - permissions: - id-token: write - steps: - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v5 - with: - role-to-assume: ${{ env.aws-role }} - aws-region: eu-west-2 - - name: Download good-job task definition artifact - uses: actions/download-artifact@v5 - with: - path: ${{ runner.temp }} - name: ${{ inputs.environment }}-good-job-task-definition - - name: Change family of task definition - run: | - file_path="${{ runner.temp }}/good-job-task-definition.json" - family_name="mavis-good-job-task-definition-${{ inputs.environment }}" - echo "$(jq --arg f "$family_name" '.family = $f' "$file_path")" > "$file_path" - - name: Deploy good-job service - uses: aws-actions/amazon-ecs-deploy-task-definition@v2 - with: - task-definition: ${{ runner.temp }}/good-job-task-definition.json - cluster: ${{ env.cluster-name }} - service: mavis-${{ inputs.environment }}-good-job - force-new-deployment: true - wait-for-service-stability: true - create-sidekiq-deployment: name: Create sidekiq deployment runs-on: ubuntu-latest if: ${{ inputs.server-types == 'sidekiq' || inputs.server-types == 'all' }} - needs: [ prepare-deployment, approve-deployments ] + needs: [prepare-deployment, approve-deployments] permissions: id-token: write steps: From 843755c5a8d4857931b23e9d2517eb61f56377b4 Mon Sep 17 00:00:00 2001 From: Theodor Vararu Date: Thu, 11 Sep 2025 17:01:02 +0700 Subject: [PATCH 15/18] Align deploy-application to data-replication task Skip building if an `image-tag` is passed in, and rename the `deploy-sidekiq` subtask for consistency. --- .github/workflows/deploy-application.yml | 39 ++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-application.yml b/.github/workflows/deploy-application.yml index a701ebbf89..977818b8f8 100644 --- a/.github/workflows/deploy-application.yml +++ b/.github/workflows/deploy-application.yml @@ -29,6 +29,10 @@ on: description: The git commit SHA to deploy. required: false type: string + image-tag: + description: Docker image tag to deploy (if not provided, will build from git-ref-to-deploy) + required: false + type: string workflow_call: inputs: environment: @@ -41,6 +45,10 @@ on: description: The git commit SHA to deploy. required: true type: string + image-tag: + description: Docker image tag to deploy + required: false + type: string permissions: {} @@ -56,9 +64,34 @@ env: app-version: ${{ inputs.git-ref-to-deploy == '' && 'unknown' || inputs.git-ref-to-deploy }} jobs: + determine-git-sha: + runs-on: ubuntu-latest + permissions: {} + outputs: + git-sha: ${{ steps.get-git-sha.outputs.git-sha }} + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + ref: ${{ inputs.git-ref-to-deploy || github.sha }} + - name: Get git sha + id: get-git-sha + run: echo "git-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + + build-and-push-image: + if: ${{ !inputs.image-tag }} + permissions: + id-token: write + needs: determine-git-sha + uses: ./.github/workflows/build-and-push-image.yml + with: + git-sha: ${{ needs.determine-git-sha.outputs.git-sha }} + prepare-deployment: name: Prepare deployment runs-on: ubuntu-latest + needs: [determine-git-sha, build-and-push-image] + if: ${{ always() && (needs.build-and-push-image.result == 'success' || needs.build-and-push-image.result == 'skipped') }} permissions: id-token: write strategy: @@ -88,7 +121,7 @@ jobs: run: | digest=$(aws ecr describe-images \ --repository-name mavis/webapp \ - --image-ids imageTag=${{ steps.checkout-code.outputs.commit }} \ + --image-ids imageTag=${{ inputs.image-tag || needs.determine-git-sha.outputs.git-sha }} \ --query 'imageDetails[0].imageDigest' \ --output text) echo "digest=$digest" >> $GITHUB_OUTPUT @@ -173,8 +206,8 @@ jobs: aws deploy wait deployment-successful --deployment-id "${{ steps.deploy-web-service.outputs.codedeploy-deployment-id }}" echo "Deployment successful" - create-sidekiq-deployment: - name: Create sidekiq deployment + deploy-sidekiq: + name: Deploy sidekiq service runs-on: ubuntu-latest if: ${{ inputs.server-types == 'sidekiq' || inputs.server-types == 'all' }} needs: [prepare-deployment, approve-deployments] From 49a9c00f9cbcd0f096ce9ab959c70e25dca24ce6 Mon Sep 17 00:00:00 2001 From: Theodor Vararu Date: Thu, 11 Sep 2025 17:12:27 +0700 Subject: [PATCH 16/18] Align refresh-data-replication to conventions Use kebab-case for variables and remove an unused env var. --- .../workflows/refresh-data-replication.yml | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/.github/workflows/refresh-data-replication.yml b/.github/workflows/refresh-data-replication.yml index be27c8d7da..3405d7ea9d 100644 --- a/.github/workflows/refresh-data-replication.yml +++ b/.github/workflows/refresh-data-replication.yml @@ -15,16 +15,16 @@ on: - qa - sandbox-alpha - sandbox-beta - db_snapshot_arn: + db-snapshot-arn: description: ARN of the DB snapshot to use (optional) required: false type: string - egress_cidr: + egress-cidr: description: CIDR blocks to allow egress traffic. type: string required: true default: "[]" - take_db_snapshot: + take-db-snapshot: description: Take a new DB snapshot before creating the environment type: boolean default: false @@ -56,13 +56,13 @@ jobs: - name: Checkout code uses: actions/checkout@v5 - name: Assume DB Snapshot role - if: inputs.take_db_snapshot - uses: aws-actions/configure-aws-credentials@v4 + if: inputs.take-db-snapshot + uses: aws-actions/configure-aws-credentials@v5 with: role-to-assume: ${{ env.db_snapshot_role }} aws-region: eu-west-2 - name: Take DB snapshot - if: inputs.take_db_snapshot + if: inputs.take-db-snapshot run: | set -e snapshot_identifier=snapshot-for-data-replication-$(date +"%Y-%m-%d-%H-%M-%S") @@ -71,15 +71,15 @@ jobs: aws rds wait db-cluster-snapshot-available --db-cluster-snapshot-identifier $snapshot_identifier echo "New snapshot is now available" - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v5 with: role-to-assume: ${{ env.aws_role }} aws-region: eu-west-2 - - name: get latest snapshot + - name: Get latest snapshot id: get-latest-snapshot run: | set -e - if [ -z "${{ inputs.db_snapshot_arn }}" ]; then + if [ -z "${{ inputs.db-snapshot-arn }}" ]; then echo "No snapshot ARN provided, fetching the latest snapshot" SNAPSHOT_ARN=$(aws rds describe-db-cluster-snapshots \ --query "DBClusterSnapshots[?DBClusterIdentifier=='mavis-${{ inputs.environment }}'].[DBClusterSnapshotArn, SnapshotCreateTime]" \ @@ -90,8 +90,8 @@ jobs: exit 1 fi else - echo "Using provided snapshot ARN: ${{ inputs.db_snapshot_arn }}" - SNAPSHOT_ARN="${{ inputs.db_snapshot_arn }}" + echo "Using provided snapshot ARN: ${{ inputs.db-snapshot-arn }}" + SNAPSHOT_ARN="${{ inputs.db-snapshot-arn }}" fi echo "Using snapshot ARN: $SNAPSHOT_ARN" echo "SNAPSHOT_ARN=$SNAPSHOT_ARN" >> $GITHUB_OUTPUT @@ -110,14 +110,13 @@ jobs: env: SNAPSHOT_ARN: ${{ needs.prepare-db-replica.outputs.SNAPSHOT_ARN }} DB_SECRET_ARN: ${{ needs.prepare-db-replica.outputs.DB_SECRET_ARN || 'arn:aws:secretsmanager:eu-west-2:000000000000:secret:placeholder' }} - REPLACE_DB_CLUSTER: ${{ inputs.deployment_type == 'Deployment with DB recreation' }} permissions: id-token: write steps: - name: Checkout code uses: actions/checkout@v5 - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v5 with: role-to-assume: ${{ env.aws_role }} aws-region: eu-west-2 @@ -138,7 +137,7 @@ jobs: set -eo pipefail terraform init -backend-config="env/${{ inputs.environment }}-backend.hcl" -upgrade - CIDR_BLOCKS='${{ inputs.egress_cidr }}' + CIDR_BLOCKS='${{ inputs.egress-cidr }}' PLAN_ARGS=( "plan" "-var=db_secret_arn=${{ steps.get-db-secret-arn.outputs.DB_SECRET_ARN }}" @@ -167,7 +166,7 @@ jobs: - name: Checkout code uses: actions/checkout@v5 - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v5 with: role-to-assume: ${{ env.aws_role }} aws-region: eu-west-2 From c9c17f5202cb4dbe0d9e57e1fa9c591c02d4252c Mon Sep 17 00:00:00 2001 From: Theodor Vararu Date: Thu, 11 Sep 2025 17:14:00 +0700 Subject: [PATCH 17/18] Remove good-job references from container vars We don't deploy this worker anymore. --- config/container_variables.yml | 14 -------------- terraform/app/env/sandbox-alpha.tfvars | 6 +++--- terraform/app/env/sandbox-beta.tfvars | 6 +++--- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/config/container_variables.yml b/config/container_variables.yml index 6369303cfc..59ae9cc93d 100644 --- a/config/container_variables.yml +++ b/config/container_variables.yml @@ -41,8 +41,6 @@ tunable_vars: MAVIS__SPLUNK__ENABLED: "true" MAVIS__CIS2__ENABLED: "true" MAVIS__ACADEMIC_YEAR_NUMBER_OF_PREPARATION_DAYS: 31 - good-job: - GOOD_JOB_MAX_THREADS: 5 sidekiq: SIDEKIQ_CONCURRENCY: 5 MAVIS__PDS__ENQUEUE_BULK_UPDATES: "true" @@ -53,8 +51,6 @@ tunable_vars: MAVIS__SPLUNK__ENABLED: "true" MAVIS__CIS2__ENABLED: "false" MAVIS__ACADEMIC_YEAR_NUMBER_OF_PREPARATION_DAYS: 45 - good-job: - GOOD_JOB_MAX_THREADS: 5 sidekiq: SIDEKIQ_CONCURRENCY: 5 MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" @@ -64,8 +60,6 @@ tunable_vars: web: MAVIS__SPLUNK__ENABLED: "true" MAVIS__CIS2__ENABLED: "true" - good-job: - GOOD_JOB_MAX_THREADS: 5 sidekiq: SIDEKIQ_CONCURRENCY: 5 MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" @@ -75,8 +69,6 @@ tunable_vars: web: MAVIS__SPLUNK__ENABLED: "false" MAVIS__CIS2__ENABLED: "false" - good-job: - GOOD_JOB_MAX_THREADS: 5 sidekiq: SIDEKIQ_CONCURRENCY: 5 MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" @@ -86,8 +78,6 @@ tunable_vars: web: MAVIS__SPLUNK__ENABLED: "false" MAVIS__CIS2__ENABLED: "false" - good-job: - GOOD_JOB_MAX_THREADS: 5 sidekiq: SIDEKIQ_CONCURRENCY: 5 MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" @@ -97,8 +87,6 @@ tunable_vars: web: MAVIS__SPLUNK__ENABLED: "false" MAVIS__CIS2__ENABLED: "false" - good-job: - GOOD_JOB_MAX_THREADS: 5 sidekiq: SIDEKIQ_CONCURRENCY: 5 MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" @@ -108,8 +96,6 @@ tunable_vars: web: MAVIS__SPLUNK__ENABLED: "false" MAVIS__CIS2__ENABLED: "false" - good-job: - GOOD_JOB_MAX_THREADS: 5 sidekiq: SIDEKIQ_CONCURRENCY: 5 MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" diff --git a/terraform/app/env/sandbox-alpha.tfvars b/terraform/app/env/sandbox-alpha.tfvars index 259710f3d0..774603168a 100644 --- a/terraform/app/env/sandbox-alpha.tfvars +++ b/terraform/app/env/sandbox-alpha.tfvars @@ -12,11 +12,11 @@ http_hosts = { MAVIS__GIVE_OR_REFUSE_CONSENT_HOST = "sandbox-alpha.mavistesting.com" } -minimum_web_replicas = 1 -maximum_web_replicas = 2 +minimum_web_replicas = 1 +maximum_web_replicas = 2 minimum_sidekiq_replicas = 1 maximum_sidekiq_replicas = 2 -good_job_replicas = 1 +good_job_replicas = 1 valkey_node_type = "cache.t4g.micro" valkey_log_retention_days = 3 diff --git a/terraform/app/env/sandbox-beta.tfvars b/terraform/app/env/sandbox-beta.tfvars index 0df3c6881f..05bec60cc6 100644 --- a/terraform/app/env/sandbox-beta.tfvars +++ b/terraform/app/env/sandbox-beta.tfvars @@ -12,11 +12,11 @@ http_hosts = { MAVIS__GIVE_OR_REFUSE_CONSENT_HOST = "sandbox-beta.mavistesting.com" } -minimum_web_replicas = 1 -maximum_web_replicas = 2 +minimum_web_replicas = 1 +maximum_web_replicas = 2 minimum_sidekiq_replicas = 1 maximum_sidekiq_replicas = 2 -good_job_replicas = 1 +good_job_replicas = 1 # Valkey serverless configuration - minimal settings for sandbox valkey_node_type = "cache.t4g.micro" From 4e6a0c942f6189a788168650e9ceb7f0fcdc3d38 Mon Sep 17 00:00:00 2001 From: Brage Gording Date: Mon, 15 Sep 2025 12:09:18 +0100 Subject: [PATCH 18/18] Change container startup variable definitions - Agreed to change the setup to control the set of tunable variables only on infrastructure side - Parameter store locations for the variables are created, but only populated with a CHANGE_ME parameter - This is for use with non-prod systems only - The docker entry point now unsets any variables with CHANGE_ME values - Workflow updated to reflect new secret/aws variable management setup - python scripts/dependency removed as no scripting is needed for secrets in the new setup --- .../workflows/data-replication-pipeline.yml | 9 -- .github/workflows/deploy-application.yml | 20 --- .tool-versions | 1 - bin/docker-entrypoint | 16 +-- config/container_variables.yml | 102 --------------- script/populate_ssm_parameters.py | 90 ------------- .../iam_policy_DeployECSServiceResources.json | 5 - terraform/app/ecs.tf | 33 ++--- terraform/app/env/sandbox-alpha.tfvars | 1 - terraform/app/env/sandbox-beta.tfvars | 1 - terraform/app/iam.tf | 16 ++- terraform/app/iam_policy_documents.tf | 49 ++++--- terraform/app/ssm_parameters.tf | 22 ++-- terraform/app/variables.tf | 121 ++++++++++++------ 14 files changed, 138 insertions(+), 348 deletions(-) delete mode 100644 config/container_variables.yml delete mode 100755 script/populate_ssm_parameters.py diff --git a/.github/workflows/data-replication-pipeline.yml b/.github/workflows/data-replication-pipeline.yml index 02895d04a1..3137c91959 100644 --- a/.github/workflows/data-replication-pipeline.yml +++ b/.github/workflows/data-replication-pipeline.yml @@ -90,13 +90,6 @@ jobs: with: role-to-assume: ${{ env.aws_role }} aws-region: eu-west-2 - - name: Setup python - uses: actions/setup-python@v4 - with: - python-version: 3.13.7 - cache: pip - - name: Install yq - run: pip install yq - name: Get image digest id: get-image-digest run: | @@ -109,10 +102,8 @@ jobs: - name: Parse environment variables id: parse-environment-variables run: | - parsed_env_vars=$(yq -r '.environments.${{ inputs.environment }} | to_entries | .[] | .key + "=" + .value' config/container_variables.yml) { echo 'parsed_env_vars<> $GITHUB_OUTPUT - - name: Parse environment variables - id: parse-environment-variables - run: | - parsed_env_vars=$(yq -r '.environments.${{ inputs.environment }} | to_entries | .[] | .key + "=" + .value' config/container_variables.yml) - { - echo 'parsed_env_vars<> "$GITHUB_OUTPUT" - name: Populate web task definition id: create-task-definition uses: aws-actions/amazon-ecs-render-task-definition@v1 @@ -141,12 +125,8 @@ jobs: task-definition-family: "mavis-${{ matrix.service }}-task-definition-${{ inputs.environment }}-template" container-name: "application" image: "${{ env.aws-account-id }}.dkr.ecr.eu-west-2.amazonaws.com/mavis/webapp@${{ steps.get-image-digest.outputs.digest }}" - environment-variables: ${{ steps.parse-environment-variables.outputs.parsed_env_vars }} - name: Rename task definition file run: mv ${{ steps.create-task-definition.outputs.task-definition }} ${{ runner.temp }}/${{ matrix.service }}-task-definition.json - - name: Populate SSM parameters for ${{ matrix.service }} service - run: | - python3 script/populate_ssm_parameters.py ${{ inputs.environment }} ${{ matrix.service }} --app-version ${{ env.app-version }} - name: Upload artifact for ${{ matrix.service }} task definition uses: actions/upload-artifact@v4 with: diff --git a/.tool-versions b/.tool-versions index 397eb65317..38af3ad9d7 100644 --- a/.tool-versions +++ b/.tool-versions @@ -3,7 +3,6 @@ hk 1.1.2 nodejs 22.15.0 pkl 0.28.1 postgres 17.2 -python 3.13.7 redis 8.2.1 ruby 3.4.3 terraform 1.11.4 diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index 88361ba027..85c12bb78d 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -6,17 +6,11 @@ if [ -z "${LD_PRELOAD+x}" ]; then export LD_PRELOAD fi - -# Extract ENV_VARS into set of variables and export them to the shell -if [ -n "$ENV_VARS" ]; then - IFS=',' read -ra ADDR <<< "$ENV_VARS" - for i in "${ADDR[@]}"; do - if [[ "$i" == *"="* ]]; then - export "${i?}" - echo "Exported: $i" - fi - done -fi +for var in $(env | cut -d= -f1); do + if [ "${!var}" = "CHANGE_ME" ]; then + unset "$var" + fi +done # When starting the container then create or migrate existing database if [ "$1" == "./bin/docker-start" ]; then diff --git a/config/container_variables.yml b/config/container_variables.yml deleted file mode 100644 index 59ae9cc93d..0000000000 --- a/config/container_variables.yml +++ /dev/null @@ -1,102 +0,0 @@ -# Container Variables Configuration -# This file specifies environment-specific variables that will be added to or override -# the environment variables extracted from terraform configuration - -environments: - production: - RAILS_ENV: production - SENTRY_ENVIRONMENT: production - - qa: - RAILS_ENV: staging - SENTRY_ENVIRONMENT: qa - - test: - RAILS_ENV: staging - SENTRY_ENVIRONMENT: test - - preview: - RAILS_ENV: staging - SENTRY_ENVIRONMENT: preview - - training: - RAILS_ENV: staging - SENTRY_ENVIRONMENT: training - - sandbox-alpha: - RAILS_ENV: staging - SENTRY_ENVIRONMENT: sandbox-alpha - - sandbox-beta: - RAILS_ENV: staging - SENTRY_ENVIRONMENT: sandbox-beta - -# Tunable Variables Configuration -# This section specifies environment-specific variables that will be populated into the SSM parameter store -# at /${environment}/envs/ and pulled on task startup. These variables can be modified in the cloud -# console or via the AWS CLI and picked up after tasks are restarted, reducing the need to redeploy. -tunable_vars: - production: - web: - MAVIS__SPLUNK__ENABLED: "true" - MAVIS__CIS2__ENABLED: "true" - MAVIS__ACADEMIC_YEAR_NUMBER_OF_PREPARATION_DAYS: 31 - sidekiq: - SIDEKIQ_CONCURRENCY: 5 - MAVIS__PDS__ENQUEUE_BULK_UPDATES: "true" - MAVIS__PDS__RATE_LIMIT_PER_SECOND: 50 - - qa: - web: - MAVIS__SPLUNK__ENABLED: "true" - MAVIS__CIS2__ENABLED: "false" - MAVIS__ACADEMIC_YEAR_NUMBER_OF_PREPARATION_DAYS: 45 - sidekiq: - SIDEKIQ_CONCURRENCY: 5 - MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" - MAVIS__PDS__RATE_LIMIT_PER_SECOND: 5 - - test: - web: - MAVIS__SPLUNK__ENABLED: "true" - MAVIS__CIS2__ENABLED: "true" - sidekiq: - SIDEKIQ_CONCURRENCY: 5 - MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" - MAVIS__PDS__RATE_LIMIT_PER_SECOND: 5 - - preview: - web: - MAVIS__SPLUNK__ENABLED: "false" - MAVIS__CIS2__ENABLED: "false" - sidekiq: - SIDEKIQ_CONCURRENCY: 5 - MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" - MAVIS__PDS__RATE_LIMIT_PER_SECOND: 5 - - training: - web: - MAVIS__SPLUNK__ENABLED: "false" - MAVIS__CIS2__ENABLED: "false" - sidekiq: - SIDEKIQ_CONCURRENCY: 5 - MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" - MAVIS__PDS__RATE_LIMIT_PER_SECOND: 5 - - sandbox-alpha: - web: - MAVIS__SPLUNK__ENABLED: "false" - MAVIS__CIS2__ENABLED: "false" - sidekiq: - SIDEKIQ_CONCURRENCY: 5 - MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" - MAVIS__PDS__RATE_LIMIT_PER_SECOND: 5 - - sandbox-beta: - web: - MAVIS__SPLUNK__ENABLED: "false" - MAVIS__CIS2__ENABLED: "false" - sidekiq: - SIDEKIQ_CONCURRENCY: 5 - MAVIS__PDS__ENQUEUE_BULK_UPDATES: "false" - MAVIS__PDS__RATE_LIMIT_PER_SECOND: 5 diff --git a/script/populate_ssm_parameters.py b/script/populate_ssm_parameters.py deleted file mode 100755 index 41c6339395..0000000000 --- a/script/populate_ssm_parameters.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import sys -import os -import boto3 -import yaml -from typing import Dict, List, Any - - -def load_yaml_config(file_path: str) -> Dict[str, Any]: - """Load and parse YAML configuration file.""" - try: - with open(file_path, 'r') as f: - return yaml.safe_load(f)['tunable_vars'] - except FileNotFoundError: - print(f"Error: Container variables file '{file_path}' not found") - sys.exit(1) - except yaml.YAMLError as e: - print(f"Error: Failed to parse YAML file '{file_path}': {e}") - sys.exit(1) - - -def extract_cloud_variables(config: Dict[str, Any], environment: str, server_type: str) -> List[str]: - """Extract cloud variables from YAML config for the given environment and server type.""" - cloud_vars = [] - - env_config = config[environment] - if server_type in env_config: - for key, value in env_config[server_type].items(): - cloud_vars.append(f"{key}={value}") - - return cloud_vars - - -def update_ssm_parameter(parameter_name: str, values: List[str], app_version: str) -> None: - """Update SSM parameter with StringList values.""" - try: - ssm = boto3.client('ssm') - values.append(f"app_version={app_version}") - string_list = ','.join(values) - - print(f"Updating SSM parameter: {parameter_name}") - - ssm.put_parameter( - Name=parameter_name, - Value=string_list, - Type='StringList', - Overwrite=True - ) - - except Exception as e: - print(f"Error: Failed to update SSM parameter '{parameter_name}': {e}") - sys.exit(1) - - -def main(): - parser = argparse.ArgumentParser( - description="Populate SSM parameter store from cloud_variables configuration", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - %(prog)s sandbox-alpha web - %(prog)s production good-job - """ - ) - - parser.add_argument('environment', help='Environment name (e.g., qa, production, etc.)') - parser.add_argument('server_type', help='Server type') - parser.add_argument('-c', '--config-file', default='config/container_variables.yml', - help='Container variables file path (default: config/container_variables.yml)') - parser.add_argument('--app-version', default='unknown', help='Application version (default: unknown)') - - args = parser.parse_args() - - # Validate config file exists - if not os.path.isfile(args.config_file): - print(f"Error: Container config file '{args.config_file}' not found") - sys.exit(1) - - config = load_yaml_config(args.config_file) - cloud_vars = extract_cloud_variables(config, args.environment, args.server_type) - - ssm_parameter_path = f"/{args.environment}/envs/{args.server_type}" - update_ssm_parameter(ssm_parameter_path, cloud_vars, args.app_version) - - print(f"Cloud variables updated successfully") - -if __name__ == '__main__': - main() diff --git a/terraform/account/resources/iam_policy_DeployECSServiceResources.json b/terraform/account/resources/iam_policy_DeployECSServiceResources.json index b30c6624e3..3b59b05cf0 100644 --- a/terraform/account/resources/iam_policy_DeployECSServiceResources.json +++ b/terraform/account/resources/iam_policy_DeployECSServiceResources.json @@ -49,11 +49,6 @@ "codedeploy:ListDeploymentTargets", "codedeploy:RegisterApplicationRevision", "codedeploy:StopDeployment", - "ssm:DescribeParameters", - "ssm:GetParameter", - "ssm:GetParameters", - "ssm:GetParametersByPath", - "ssm:PutParameter", "iam:PassRole" ], "Resource": ["*"] diff --git a/terraform/app/ecs.tf b/terraform/app/ecs.tf index 4b9ca4b09e..5b41f241ec 100644 --- a/terraform/app/ecs.tf +++ b/terraform/app/ecs.tf @@ -22,16 +22,11 @@ resource "aws_ecs_cluster" "cluster" { module "web_service" { source = "./modules/ecs_service" task_config = { - environment = local.task_envs - secrets = concat( - local.task_secrets, - [{ - name = "ENV_VARS" - valueFrom = aws_ssm_parameter.cloud_variables["web"].arn - }]) + environment = local.task_envs["CORE"] + secrets = local.task_secrets["CORE"] cpu = 1024 memory = 2048 - execution_role_arn = aws_iam_role.ecs_task_execution_role.arn + execution_role_arn = aws_iam_role.ecs_task_execution_role["CORE"].arn task_role_arn = aws_iam_role.ecs_task_role.arn log_group_name = aws_cloudwatch_log_group.ecs_log_group.name region = var.region @@ -65,16 +60,11 @@ module "web_service" { module "good_job_service" { source = "./modules/ecs_service" task_config = { - environment = local.task_envs - secrets = concat( - local.task_secrets, - [{ - name = "ENV_VARS" - valueFrom = aws_ssm_parameter.cloud_variables["good-job"].arn - }]) + environment = local.task_envs["CORE"] + secrets = local.task_secrets["CORE"] cpu = 1024 memory = 2048 - execution_role_arn = aws_iam_role.ecs_task_execution_role.arn + execution_role_arn = aws_iam_role.ecs_task_execution_role["CORE"].arn task_role_arn = aws_iam_role.ecs_task_role.arn log_group_name = aws_cloudwatch_log_group.ecs_log_group.name region = var.region @@ -95,16 +85,11 @@ module "good_job_service" { module "sidekiq_service" { source = "./modules/ecs_service" task_config = { - environment = local.task_envs - secrets = concat( - local.task_secrets, - [{ - name = "ENV_VARS" - valueFrom = aws_ssm_parameter.cloud_variables["sidekiq"].arn - }]) + environment = local.task_envs["CORE"] + secrets = local.task_secrets["CORE"] cpu = 1024 memory = 2048 - execution_role_arn = aws_iam_role.ecs_task_execution_role.arn + execution_role_arn = aws_iam_role.ecs_task_execution_role["CORE"].arn task_role_arn = aws_iam_role.ecs_task_role.arn log_group_name = aws_cloudwatch_log_group.ecs_log_group.name region = var.region diff --git a/terraform/app/env/sandbox-alpha.tfvars b/terraform/app/env/sandbox-alpha.tfvars index 774603168a..435c0ee86c 100644 --- a/terraform/app/env/sandbox-alpha.tfvars +++ b/terraform/app/env/sandbox-alpha.tfvars @@ -21,4 +21,3 @@ good_job_replicas = 1 valkey_node_type = "cache.t4g.micro" valkey_log_retention_days = 3 valkey_failover_enabled = false -sidekiq_replicas = 1 diff --git a/terraform/app/env/sandbox-beta.tfvars b/terraform/app/env/sandbox-beta.tfvars index 05bec60cc6..f7145ada79 100644 --- a/terraform/app/env/sandbox-beta.tfvars +++ b/terraform/app/env/sandbox-beta.tfvars @@ -22,4 +22,3 @@ good_job_replicas = 1 valkey_node_type = "cache.t4g.micro" valkey_log_retention_days = 3 valkey_failover_enabled = false -sidekiq_replicas = 1 diff --git a/terraform/app/iam.tf b/terraform/app/iam.tf index f9a42c11f9..824d3565f7 100644 --- a/terraform/app/iam.tf +++ b/terraform/app/iam.tf @@ -1,7 +1,8 @@ ################################# IAM Policies ################################# resource "aws_iam_policy" "ecs_secret_access_policy" { - name = "ecs-secret-access-policy-${var.environment}" - policy = data.aws_iam_policy_document.ecs_secrets_access.json + for_each = local.non_empty_parameter_groups + name = "ecs-secret-access-policy-${var.environment}-${each.key}" + policy = data.aws_iam_policy_document.ecs_secrets_access[each.key].json } resource "aws_iam_policy" "shell_access_policy" { @@ -23,7 +24,8 @@ resource "aws_iam_policy" "vpc_flowlogs" { resource "aws_iam_role" "ecs_task_execution_role" { - name = "ecsTaskExecutionRole-${var.environment}" + for_each = local.parameter_store_variables + name = "ecsTaskExecutionRole-${var.environment}-${each.key}" assume_role_policy = templatefile("templates/iam_assume_role.json.tpl", { service_name = "ecs-tasks.amazonaws.com" }) } @@ -47,12 +49,14 @@ resource "aws_iam_role" "vpc_flowlogs" { ################################# IAM Role/Policy Attachments ################################# resource "aws_iam_role_policy_attachment" "ecs_secret_access" { - role = aws_iam_role.ecs_task_execution_role.name - policy_arn = aws_iam_policy.ecs_secret_access_policy.arn + for_each = local.non_empty_parameter_groups + role = aws_iam_role.ecs_task_execution_role[each.key].name + policy_arn = aws_iam_policy.ecs_secret_access_policy[each.key].arn } resource "aws_iam_role_policy_attachment" "ecs_ecr_and_log_permissions" { - role = aws_iam_role.ecs_task_execution_role.name + for_each = local.parameter_store_variables + role = aws_iam_role.ecs_task_execution_role[each.key].name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } diff --git a/terraform/app/iam_policy_documents.tf b/terraform/app/iam_policy_documents.tf index 3ec600e94c..932f86cdc4 100644 --- a/terraform/app/iam_policy_documents.tf +++ b/terraform/app/iam_policy_documents.tf @@ -19,18 +19,16 @@ data "aws_iam_policy_document" "codedeploy" { "elasticloadbalancing:DescribeRules", "elasticloadbalancing:ModifyRule" ] - resources = ["*"] #TODO: Restrict permissions to only Mavis-specifc resources - effect = "Allow" - } - statement { - actions = ["s3:GetObject", "s3:GetObjectVersion"] - resources = ["arn:aws:s3:::*"] + resources = ["*"] effect = "Allow" } statement { - actions = ["iam:PassRole"] - resources = [aws_iam_role.ecs_task_role.arn, aws_iam_role.ecs_task_execution_role.arn] - effect = "Allow" + actions = ["iam:PassRole"] + resources = concat( + [aws_iam_role.ecs_task_role.arn], + [for role in aws_iam_role.ecs_task_execution_role : role.arn] + ) + effect = "Allow" } } @@ -48,23 +46,24 @@ data "aws_iam_policy_document" "shell_access" { } data "aws_iam_policy_document" "ecs_secrets_access" { - statement { - sid = "ssmParameterStoreAccessSid" - actions = ["ssm:GetParameters"] - resources = concat( - ["arn:aws:ssm:${var.region}:${var.account_id}:parameter${var.rails_master_key_path}"], - local.parameter_store_arns, #TODO: Remove once all variables are sourced from application config - [for key, value in aws_ssm_parameter.cloud_variables : value.arn] - ) - effect = "Allow" + for_each = local.non_empty_parameter_groups + dynamic "statement" { + for_each = length(local.parameter_store_paths[each.key]) == 0 ? [] : [1] + content { + sid = "ssmParameterStoreAccessSid" + actions = ["ssm:GetParameters"] + resources = [for path in local.parameter_store_paths[each.key] : "arn:aws:ssm:${var.region}:${var.account_id}:parameter${path}"] + effect = "Allow" + } } - statement { - sid = "dbSecretSid" - actions = ["secretsmanager:GetSecretValue"] - resources = [ - aws_rds_cluster.core.master_user_secret[0].secret_arn - ] - effect = "Allow" + dynamic "statement" { + for_each = length(local.secret_arns[each.key]) == 0 ? [] : [1] + content { + sid = "dbSecretSid" + actions = ["secretsmanager:GetSecretValue"] + resources = local.secret_arns[each.key] + effect = "Allow" + } } } diff --git a/terraform/app/ssm_parameters.tf b/terraform/app/ssm_parameters.tf index 412121dc1b..58082cc06c 100644 --- a/terraform/app/ssm_parameters.tf +++ b/terraform/app/ssm_parameters.tf @@ -1,6 +1,6 @@ -resource "aws_ssm_parameter" "environment_config" { #TODO: Remove once all variables are sourced from application config - for_each = local.parameter_store_variables - name = "/${var.environment}/env/${each.key}" +resource "aws_ssm_parameter" "core_environment_overwrites" { + for_each = local.parameter_store_variables["CORE"] + name = "/${var.environment}/env/core/${each.key}" type = "String" value = each.value @@ -9,17 +9,13 @@ resource "aws_ssm_parameter" "environment_config" { #TODO: Remove once all varia } } -resource "aws_ssm_parameter" "cloud_variables" { - for_each = toset([ - "web", "good-job", "sidekiq" - ]) - name = "/${var.environment}/envs/${each.value}" - type = "StringList" - value = "service=${each.value}" +resource "aws_ssm_parameter" "reporting_environment_overwrites" { + for_each = local.parameter_store_variables["REPORTING"] + name = "/${var.environment}/env/reporting/${each.key}" + type = "String" + value = each.value lifecycle { - ignore_changes = [ - value - ] + ignore_changes = all } } diff --git a/terraform/app/variables.tf b/terraform/app/variables.tf index 8962f638ca..142022e624 100644 --- a/terraform/app/variables.tf +++ b/terraform/app/variables.tf @@ -113,15 +113,35 @@ variable "enable_enhanced_db_monitoring" { locals { is_production = var.environment == "production" - parameter_store_variables = tomap({ #TODO: Remove once all variables are sourced from application config - MAVIS__ACADEMIC_YEAR_TODAY_OVERRIDE = "" - MAVIS__ACADEMIC_YEAR_NUMBER_OF_PREPARATION_DAYS = "" - MAVIS__PDS__ENQUEUE_BULK_UPDATES = "" - MAVIS__PDS__RATE_LIMIT_PER_SECOND = 5 - GOOD_JOB_MAX_THREADS = 5 - SIDEKIQ_CONCURRENCY = 5 + parameter_store_variables = tomap({ + CORE = local.is_production ? {} : tomap({ + MAVIS__ACADEMIC_YEAR_TODAY_OVERRIDE = "CHANGE_ME" + MAVIS__ACADEMIC_YEAR_NUMBER_OF_PREPARATION_DAYS = "CHANGE_ME" + MAVIS__PDS__ENQUEUE_BULK_UPDATES = "CHANGE_ME" + MAVIS__PDS__RATE_LIMIT_PER_SECOND = "CHANGE_ME" + SIDEKIQ_CONCURRENCY = "CHANGE_ME" + }) + REPORTING = local.is_production ? {} : tomap({ + }) }) - parameter_store_arns = [for key, value in aws_ssm_parameter.environment_config : value.arn] #TODO: Remove once all variables are sourced from application config + non_empty_parameter_groups = toset([ + for key, value in local.parameter_store_variables : key if length(concat(local.secret_arns[key], local.parameter_store_paths[key])) > 0 + ]) + parameter_store_paths = tomap( + { + CORE = concat( + [for key, value in aws_ssm_parameter.core_environment_overwrites : value.name], + [var.rails_master_key_path] + ) + REPORTING = [for key, value in aws_ssm_parameter.reporting_environment_overwrites : value.name] + } + ) + secret_arns = tomap( + { + CORE = [aws_rds_cluster.core.master_user_secret[0].secret_arn] + REPORTING = [] + } + ) sandbox_envs = ( startswith(var.environment, "sandbox") ? [ @@ -132,39 +152,60 @@ locals { ] : [] ) - task_envs = concat([ - { - name = "DB_HOST" - value = aws_rds_cluster.core.endpoint - }, - { - name = "DB_NAME" - value = aws_rds_cluster.core.database_name - }, - { - name = "MAVIS__HOST" - value = var.http_hosts.MAVIS__HOST - }, - { - name = "MAVIS__GIVE_OR_REFUSE_CONSENT_HOST" - value = var.http_hosts.MAVIS__GIVE_OR_REFUSE_CONSENT_HOST - }, - { - name = "SIDEKIQ_REDIS_URL" - value = "rediss://${aws_elasticache_replication_group.valkey.primary_endpoint_address}:${var.valkey_port}" - }, - ], local.sandbox_envs) + task_envs = tomap({ + CORE = concat([ + { + name = "RAILS_ENV" + value = local.is_production ? "production" : "staging" + }, + { + name = "SENTRY_ENVIRONMENT" + value = var.environment + }, + { + name = "DB_HOST" + value = aws_rds_cluster.core.endpoint + }, + { + name = "DB_NAME" + value = aws_rds_cluster.core.database_name + }, + { + name = "MAVIS__HOST" + value = var.http_hosts.MAVIS__HOST + }, + { + name = "MAVIS__GIVE_OR_REFUSE_CONSENT_HOST" + value = var.http_hosts.MAVIS__GIVE_OR_REFUSE_CONSENT_HOST + }, + { + name = "SIDEKIQ_REDIS_URL" + value = "rediss://${aws_elasticache_replication_group.valkey.primary_endpoint_address}:${var.valkey_port}" + }, + ], + local.sandbox_envs, + ) + REPORTING = [] + }) - task_secrets = [ - { - name = "DB_CREDENTIALS" - valueFrom = aws_rds_cluster.core.master_user_secret[0].secret_arn - }, - { - name = "RAILS_MASTER_KEY" - valueFrom = var.rails_master_key_path - } - ] + task_secrets = tomap({ + CORE = concat( + [ + { + name = "DB_CREDENTIALS" + valueFrom = aws_rds_cluster.core.master_user_secret[0].secret_arn + } + ], + [for key, value in local.parameter_store_variables["CORE"] : { + name = key + valueFrom = aws_ssm_parameter.core_environment_overwrites[key].name + }] + ) + REPORTING = [for key, value in local.parameter_store_variables["REPORTING"] : { + name = key + valueFrom = aws_ssm_parameter.core_environment_overwrites[key].name + }] + }) } ########## RDS configuration ##########