From ea2ebfb99baa6523d5d53a33bc760a60e7d4f252 Mon Sep 17 00:00:00 2001 From: Joseph Yousefpour Date: Thu, 23 Oct 2025 09:18:14 -0400 Subject: [PATCH] adding initial phase of ecs deployment --- .dockerignore | 4 + config/credentials.yml.enc | 1 - deployment/ecs.zip | Bin 17155 -> 0 bytes deployment/ecs/.gitignore | 1 - deployment/ecs/backend-prod.hcl | 4 +- deployment/ecs/envs/bo/.gitignore | 2 + deployment/ecs/envs/bo/app_env_local_file.tf | 53 +++ deployment/ecs/envs/bo/backend.hcl | 2 +- deployment/ecs/envs/bo/main.tf | 101 +++--- deployment/ecs/envs/bo/outputs.tf | 31 ++ deployment/ecs/envs/bo/rds.tf | 97 ++++++ .../ecs/envs/bo/templates/app_env.json.tmpl | 26 ++ deployment/ecs/envs/bo/terraform.tfvars | 52 +-- deployment/ecs/envs/bo/variables.tf | 311 ++++++++++++++++-- deployment/ecs/main.tf | 44 ++- deployment/ecs/modules/ecs/main.tf | 133 ++++++-- deployment/ecs/modules/ecs/outputs.tf | 5 + deployment/ecs/modules/ecs/variables.tf | 34 +- deployment/ecs/outputs.tf | 78 +---- deployment/ecs/variables.tf | 231 +------------ deployment/ecs/waf.tf | 40 ++- docker-compose.yaml | 40 +-- docker/app/Dockerfile | 115 +++++-- docker/app/entrypoint.sh | 9 - rebuild_docker.sh | 16 +- 25 files changed, 881 insertions(+), 549 deletions(-) create mode 100644 .dockerignore delete mode 100644 config/credentials.yml.enc delete mode 100644 deployment/ecs.zip create mode 100644 deployment/ecs/envs/bo/.gitignore create mode 100644 deployment/ecs/envs/bo/app_env_local_file.tf create mode 100644 deployment/ecs/envs/bo/outputs.tf create mode 100644 deployment/ecs/envs/bo/rds.tf create mode 100644 deployment/ecs/envs/bo/templates/app_env.json.tmpl diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..b9b7b3482 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +deployment/ +*.tf +*.log +.git/ diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc deleted file mode 100644 index 036827822..000000000 --- a/config/credentials.yml.enc +++ /dev/null @@ -1 +0,0 @@ -owsEG/FJlm9Yhw3L2rI1MShG0a+AzpRyn1UhJBnriW5u/ox/pPY0NBM6P/zBy3kKG6JaDXctFHOtmY1CW/cNOYT210S/wSa6R5eSHCTYgTPwVTrilELU/vuA6ivCxWBPTLSFz8rxJ0mJRKErgnYro06BLd3oK4SPizgmSJD59WOG7iCENeELb8f4JWhOolNWKHR/eggiKPWHU0OCFPt/RyI/y8wsmnP67188ChEBqnbgldfe6UaJlvS7JWibRutDPQrdrrRvT7gin7/sM2N7dDV+gwbXvCZkL5tCXlnVii9OpcdBYfD7zzcnwmwsKa4AQ/lPseYfHF4HlQgo33aG8L9+PQKVnPqkmJHpXh1db9zXkPGfwb16MUTQ8JXLcNPl9ZFZwSZ1o696+su9Hn4c3Fh9SWju2bG3S5+U--qYgY+cOz6SaDxjwW--kl+jhp0mvm8Faf4GczjICg== \ No newline at end of file diff --git a/deployment/ecs.zip b/deployment/ecs.zip deleted file mode 100644 index b3e80cc6f154ca8eaa99cb4049e7b15245cc4731..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17155 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 diff --git a/deployment/ecs/.gitignore b/deployment/ecs/.gitignore index 4260bbcff..22666290f 100644 --- a/deployment/ecs/.gitignore +++ b/deployment/ecs/.gitignore @@ -3,7 +3,6 @@ .sql # Local .terraform directories **/.terraform/* - .terraform* .terraform.lock.hcl # .tfstate files diff --git a/deployment/ecs/backend-prod.hcl b/deployment/ecs/backend-prod.hcl index 87355e40b..a89f3aa6f 100644 --- a/deployment/ecs/backend-prod.hcl +++ b/deployment/ecs/backend-prod.hcl @@ -1,5 +1,5 @@ -bucket = "mpath-terraform-state" -key = "mpath/ecs/vpc/terraform.tfstate" +bucket = "mpath-terraform-remote-state" +key = "mpath/vpc/terraform.tfstate" region = "us-east-1" encrypt = true use_lockfile = true \ No newline at end of file diff --git a/deployment/ecs/envs/bo/.gitignore b/deployment/ecs/envs/bo/.gitignore new file mode 100644 index 000000000..54f13ebe4 --- /dev/null +++ b/deployment/ecs/envs/bo/.gitignore @@ -0,0 +1,2 @@ +app_env.json +app_env*.json diff --git a/deployment/ecs/envs/bo/app_env_local_file.tf b/deployment/ecs/envs/bo/app_env_local_file.tf new file mode 100644 index 000000000..9a1fbab85 --- /dev/null +++ b/deployment/ecs/envs/bo/app_env_local_file.tf @@ -0,0 +1,53 @@ +data "aws_secretsmanager_secret_version" "db" { + secret_id = aws_secretsmanager_secret.db.id +} + +locals { + db_secret = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string) + db_host_from_rds = aws_db_instance.this.address + db_port_from_rds = aws_db_instance.this.port + + effective_db_host = local.db_host_from_rds != "" ? local.db_host_from_rds : try(local.db_secret.host, "") + effective_db_port = local.db_port_from_rds != 0 ? local.db_port_from_rds : try(local.db_secret.port, 3306) +} + +resource "local_file" "app_env_json" { + filename = "${path.module}/app_env.json" + + content = templatefile("${path.module}/templates/app_env.json.tmpl", { + rails_env = var.rails_env + rails_log_to_stdout = var.rails_log_to_stdout + rails_serve_static = var.rails_serve_static + puma_port = var.puma_port + web_concurrency = var.web_concurrency + rails_max_threads = var.rails_max_threads + rails_min_threads = var.rails_min_threads + + # handle null secret_key_base + secret_key_base = try(coalesce(var.secret_key_base, ""), "") + + # Database fields (read-only from Secrets Manager + RDS) + db_name = try(local.db_secret.database, var.db_name) + db_host = local.effective_db_host + db_port = local.effective_db_port + db_user = local.db_secret.username + db_password = local.db_secret.password + + # Office365 / SSO + office365_client_id = var.office365_client_id + office365_client_secret = var.office365_client_secret + office365_redirect_uri = var.office365_redirect_uri + office365_provider_url = var.office365_provider_url + + # Keycloak + keycloak_client_id = var.keycloak_client_id + keycloak_client_secret = var.keycloak_client_secret + keycloak_realm = var.keycloak_realm + keycloak_server_url = var.keycloak_server_url + + # SSL flag + use_ssl = var.use_ssl + }) + + file_permission = "0600" +} diff --git a/deployment/ecs/envs/bo/backend.hcl b/deployment/ecs/envs/bo/backend.hcl index 2a44389ee..149448264 100644 --- a/deployment/ecs/envs/bo/backend.hcl +++ b/deployment/ecs/envs/bo/backend.hcl @@ -1,4 +1,4 @@ -bucket = "mpath-terraform-state" +bucket = "mpath-terraform-remote-state" key = "mpath/ecs/bo/terraform.tfstate" region = "us-east-1" encrypt = true diff --git a/deployment/ecs/envs/bo/main.tf b/deployment/ecs/envs/bo/main.tf index bed591789..572aa5ad8 100644 --- a/deployment/ecs/envs/bo/main.tf +++ b/deployment/ecs/envs/bo/main.tf @@ -5,21 +5,24 @@ terraform { backend "s3" {} # init with: terraform init -backend-config=backend.hcl } -provider "aws" { region = var.aws_region } +provider "aws" { + region = var.aws_region +} locals { app_name = "mpath" - env = "bo" + env = var.environment } + # Pull shared network from the ROOT stack data "terraform_remote_state" "root" { backend = "s3" config = { - bucket = "mpath-terraform-state" - key = "mpath/root/terraform.tfstate" - region = var.aws_region - encrypt = true + bucket = "mpath-terraform-remote-state" + key = "mpath/vpc/terraform.tfstate" + region = var.aws_region + encrypt = true } } @@ -35,56 +38,63 @@ locals { } } + +data "aws_secretsmanager_secret_version" "mpath_cert" { + secret_id = "/mpath/acm/cert-arn" +} + +locals { + _raw_cert_string = data.aws_secretsmanager_secret_version.mpath_cert.secret_string + _maybe_json = try(jsondecode(local._raw_cert_string), null) + _json_cert_arn = local._maybe_json == null ? "" : try(local._maybe_json.cert_arn, "") + acm_cert_from_sm = local._json_cert_arn != "" ? local._json_cert_arn : local._raw_cert_string + + # Final selection: explicit var wins; else Secrets Manager + selected_acm_cert_arn = var.acm_certificate_arn != "" ? var.acm_certificate_arn : local.acm_cert_from_sm + # Or, if you’re worried about whitespace: + # selected_acm_cert_arn = trimspace(var.acm_certificate_arn) != "" ? var.acm_certificate_arn : local.acm_cert_from_sm +} + + + module "ecs_service" { source = "../../modules/ecs" - cluster_name = "${local.app_name}-${local.env}" service_name = "${local.app_name}-app-${local.env}" - - vpc_id = local.vpc_id - subnet_ids = local.private_subnet_ids # ECS tasks -> private subnets - + vpc_id = local.vpc_id + subnet_ids = local.private_subnet_ids + db_secret_arn = aws_secretsmanager_secret.db.arn + app_secret_arn = aws_secretsmanager_secret.app.arn # Container / service container_image = var.container_image - container_port = var.container_port # set to 8443 in terraform.tfvars + container_port = var.container_port desired_count = var.desired_count cpu = var.cpu memory = var.memory health_check_path = var.health_check_path - # Create ALB/TG/listeners *inside* the module (per-env ALB) - create_alb = true - public_subnet_ids = local.public_subnet_ids - alb_name = "${local.app_name}-${local.env}-alb" + # ALB/TG/listeners inside the module (per-env ALB) + create_alb = true + public_subnet_ids = local.public_subnet_ids + alb_name = "${local.app_name}-${local.env}-alb" alb_deletion_protection = true - acm_certificate_arn = var.acm_certificate_arn - ssl_policy = var.ssl_policy + acm_certificate_arn = local.selected_acm_cert_arn + ssl_policy = var.ssl_policy # Ops & deployments - platform_version = var.platform_version - deployment_maximum_percent = var.deployment_maximum_percent - deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent - deployment_circuit_breaker_enabled = var.deployment_circuit_breaker_enabled - deployment_circuit_breaker_rollback = var.deployment_circuit_breaker_rollback - container_insights_enabled = true - log_retention_days = var.log_retention_days - assign_public_ip = false + platform_version = var.platform_version + deployment_maximum_percent = var.deployment_maximum_percent + deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent + deployment_circuit_breaker_enabled = var.deployment_circuit_breaker_enabled + deployment_circuit_breaker_rollback = var.deployment_circuit_breaker_rollback + container_insights_enabled = true + log_retention_days = var.log_retention_days + assign_public_ip = false tags = local.tags } -output "bo_alb_dns_name" { - value = module.ecs_service.alb_dns_name -} - -output "bo_target_group_arn" { - value = module.ecs_service.target_group_arn_effective -} - -output "bo_ecs_service_sg" { - value = module.ecs_service.security_group_id -} # Discover this env’s ALB (created by module.ecs_service) data "aws_lb" "bo_alb" { @@ -98,3 +108,20 @@ resource "aws_wafv2_web_acl_association" "mpath_web_acl_assoc" { web_acl_arn = data.terraform_remote_state.root.outputs.waf_web_acl_arn } +resource "random_password" "secret_key_base" { + length = 64 + special = false +} + +resource "aws_secretsmanager_secret" "app" { + name = "mpath/bo/app" + description = "mPATH BO app env" +} + +resource "aws_secretsmanager_secret_version" "app" { + secret_id = aws_secretsmanager_secret.app.id + secret_string = jsonencode({ + SECRET_KEY_BASE = random_password.secret_key_base.result + # ...other keys... + }) +} diff --git a/deployment/ecs/envs/bo/outputs.tf b/deployment/ecs/envs/bo/outputs.tf new file mode 100644 index 000000000..c48093ff5 --- /dev/null +++ b/deployment/ecs/envs/bo/outputs.tf @@ -0,0 +1,31 @@ +output "rds_endpoint" { + description = "Full RDS endpoint (hostname:port)." + value = aws_db_instance.this.endpoint +} + +output "rds_sg_id" { + description = "Security Group ID attached to the RDS instance." + value = aws_security_group.rds_mysql.id +} + +output "rds_db_name" { + description = "Name of the database created." + value = var.db_name +} + +output "db_secret_arn" { + description = "Secrets Manager secret ARN with DB credentials." + value = aws_secretsmanager_secret.db.arn +} + +output "bo_alb_dns_name" { + value = module.ecs_service.alb_dns_name +} + +output "bo_target_group_arn" { + value = module.ecs_service.target_group_arn_effective +} + +output "bo_ecs_service_sg" { + value = module.ecs_service.security_group_id +} \ No newline at end of file diff --git a/deployment/ecs/envs/bo/rds.tf b/deployment/ecs/envs/bo/rds.tf new file mode 100644 index 000000000..a8fb4bd7e --- /dev/null +++ b/deployment/ecs/envs/bo/rds.tf @@ -0,0 +1,97 @@ +# Password (only used if var.db_password == null) +resource "random_password" "db" { + length = 24 + special = true + override_special = "!@#%^*-_=+" +} + +locals { + db_password_final = coalesce(var.db_password, random_password.db.result) + derived_ecs_tasks_sg_name = coalesce(var.ecs_tasks_sg_name, "mpath-${local.env}-ecs-tasks-sg") +} + +# DB Subnet Group (use private subnets from main.tf locals) +resource "aws_db_subnet_group" "this" { + name = "${var.db_identifier}-subnets" + subnet_ids = local.private_subnet_ids + tags = merge(local.tags, { Name = "${var.db_identifier}-subnets" }) +} + +# Security Group for RDS – allow MySQL from ECS tasks only +resource "aws_security_group" "rds_mysql" { + name = "${var.db_identifier}-sg" + description = "Allow MySQL from ECS tasks" + vpc_id = local.vpc_id + + ingress { + from_port = 3306 + to_port = 3306 + protocol = "tcp" + security_groups = [module.ecs_service.service_sg_id] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge(local.tags, { Name = "${var.db_identifier}-sg" }) +} + +# RDS Instance – MySQL 8, db.t3.micro, gp2, single-AZ +resource "aws_db_instance" "this" { + identifier = var.db_identifier + engine = "mysql" + engine_version = "8.0" # safe major pin + instance_class = "db.t3.micro" + + storage_type = "gp2" + allocated_storage = var.db_allocated_storage + max_allocated_storage = 100 + + db_subnet_group_name = aws_db_subnet_group.this.name + vpc_security_group_ids = [aws_security_group.rds_mysql.id] + port = 3306 + + # Credentials + username = var.db_username + password = local.db_password_final + db_name = var.db_name + + # Availability & security + multi_az = false + publicly_accessible = false + storage_encrypted = true + + # Backups & lifecycle + backup_retention_period = 7 + deletion_protection = false + skip_final_snapshot = false + + apply_immediately = true + + tags = merge(local.tags, { Name = var.db_identifier }) +} + +# Secrets Manager – JSON bundle for ECS injection +resource "aws_secretsmanager_secret" "db" { + name = var.secret_name + description = "mPATH ${upper(local.env)} DB connection" + kms_key_id = var.kms_key_id + tags = merge(local.tags, { Name = var.secret_name }) +} + +resource "aws_secretsmanager_secret_version" "db" { + secret_id = aws_secretsmanager_secret.db.id + secret_string = jsonencode({ + adapter = "mysql2" + username = var.db_username + password = local.db_password_final + host = aws_db_instance.this.address + port = 3306 + database = var.db_name + url = "mysql2://${var.db_username}:${local.db_password_final}@${aws_db_instance.this.address}:3306/${var.db_name}?encoding=utf8mb4&ssl_mode=required" + }) +} diff --git a/deployment/ecs/envs/bo/templates/app_env.json.tmpl b/deployment/ecs/envs/bo/templates/app_env.json.tmpl new file mode 100644 index 000000000..5abdc9d29 --- /dev/null +++ b/deployment/ecs/envs/bo/templates/app_env.json.tmpl @@ -0,0 +1,26 @@ +{ + "RAILS_ENV": "${rails_env}", + "RAILS_LOG_TO_STDOUT": ${rails_log_to_stdout}, + "RAILS_SERVE_STATIC_FILES": ${rails_serve_static}, + "PUMA_PORT": ${puma_port}, + "WEB_CONCURRENCY": ${web_concurrency}, + "RAILS_MAX_THREADS": ${rails_max_threads}, + "RAILS_MIN_THREADS": ${rails_min_threads}, + + "SECRET_KEY_BASE": "${secret_key_base}", + + "DATABASE_URL": "", + "DATABASE_NAME": "${db_name}", + "DATABASE_HOST": "${db_host}", + "DATABASE_PORT": ${db_port}, + "DATABASE_USER": "${db_user}", + "DATABASE_USERNAME": "${db_user}", + "DATABASE_PASSWORD": "${db_password}", + + "OFFICE365_CLIENT_ID": "${office365_client_id}", + "OFFICE365_CLIENT_SECRET": "${office365_client_secret}", + "OFFICE365_REDIRECT_URI": "${office365_redirect_uri}", + "OFFICE365_PROVIDER_URL": "${office365_provider_url}", + + "USE_SSL": ${use_ssl} +} diff --git a/deployment/ecs/envs/bo/terraform.tfvars b/deployment/ecs/envs/bo/terraform.tfvars index cffd85b93..6da5d5038 100644 --- a/deployment/ecs/envs/bo/terraform.tfvars +++ b/deployment/ecs/envs/bo/terraform.tfvars @@ -1,39 +1,47 @@ -waf_alb_arns = [ - aws_lb.mpath_production_alb.arn -] - aws_region = "us-east-1" -environment = "bo" # your env code uses local.env = "bo" - -# Container / service -container_image = "295669632222.dkr.ecr.us-east-1.amazonaws.com/microhealthllc/mpath-bo:latest" -container_port = 8443 +environment = "bo" + +# --- RDS (cheap single-AZ) --- +db_identifier = "mpath-bo-mysql" +db_name = "mpath_prod" +db_username = "mpath_admin" +db_password = null # leave null => auto-generate + store in Secrets Manager +db_allocated_storage = 20 # gp2 minimum +secret_name = "mpath/bo/db" +kms_key_id = null # or "arn:aws:kms:us-east-1:ACCOUNT:key/...." + +# If your ECS tasks SG name is NOT "mpath-bo-ecs-tasks-sg", uncomment & set: +# ecs_tasks_sg_name = "your-ecs-tasks-sg-name" + +# --- Container / service (unchanged) --- +container_image = "295669632222.dkr.ecr.us-east-1.amazonaws.com/microhealthllc/mpath-bo:latest-working" desired_count = 2 cpu = 1024 memory = 2048 -# Healthcheck -health_check_path = "/health" +# --- Healthcheck --- +health_check_path = "/users/sign_in" -# TLS / ALB -acm_certificate_arn = "" # ACM ARN -ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" +ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" alb_deletion_protection = true -# ECS deployment knobs -platform_version = "LATEST" -deployment_maximum_percent = 200 -deployment_minimum_healthy_percent = 50 -deployment_circuit_breaker_enabled = true +# --- ECS deployment knobs --- +platform_version = "LATEST" +deployment_maximum_percent = 200 +deployment_minimum_healthy_percent = 50 +deployment_circuit_breaker_enabled = true deployment_circuit_breaker_rollback = true -log_retention_days = 30 - +log_retention_days = 30 waf_allowed_countries = ["US"] -# Tags +# --- Tags (add App/Env for SG auto-discovery) --- tags = { + App = "mPATH" + Env = "bo" Owner = "DevOps Team" CostCenter = "Engineering" Application = "mpath" } + +db_secret_arn = aws_secretsmanager_secret.db.arn diff --git a/deployment/ecs/envs/bo/variables.tf b/deployment/ecs/envs/bo/variables.tf index b58c58285..fc8276378 100644 --- a/deployment/ecs/envs/bo/variables.tf +++ b/deployment/ecs/envs/bo/variables.tf @@ -1,30 +1,289 @@ -variable "aws_region" { type = string } - -# Container / service -variable "container_image" { type = string } -variable "container_port" { type = number default = 8443 } # app listens on 8443 -variable "desired_count" { type = number default = 2 } -variable "cpu" { type = number default = 512 } -variable "memory" { type = number default = 1024 } -variable "health_check_path" { type = string default = "/health" } - -# TLS for ALB -variable "acm_certificate_arn" { type = string } -variable "ssl_policy" { type = string default = "ELBSecurityPolicy-TLS-1-2-2017-01" } - -# deployments -variable "log_retention_days" { type = number default = 30 } -variable "platform_version" { type = string default = "LATEST" } -variable "deployment_maximum_percent" { type = number default = 200 } -variable "deployment_minimum_healthy_percent"{ type = number default = 50 } -variable "deployment_circuit_breaker_enabled"{ type = bool default = true } -variable "deployment_circuit_breaker_rollback"{ type = bool default = true } +# ===================================================================== +# Region +# ===================================================================== +variable "aws_region" { + description = "AWS region to deploy resources" + type = string + default = "us-east-1" +} + +# ===================================================================== +# Container / Service configuration +# ===================================================================== +variable "container_image" { + description = "ECR image URI for the ECS task" + type = string +} + +variable "container_port" { + description = "Port that the container exposes (must match app listener)" + type = number + default = 8443 +} + +variable "desired_count" { + description = "Desired number of running ECS tasks" + type = number + default = 2 +} + +variable "cpu" { + description = "CPU units for the ECS task (256, 512, 1024, etc.)" + type = number + default = 512 +} + +variable "memory" { + description = "Memory for the ECS task (in MB)" + type = number + default = 1024 +} + +variable "health_check_path" { + description = "Path used for ALB health checks" + type = string + default = "/users/sign_in" +} + +# ===================================================================== +# TLS / ALB configuration +# ===================================================================== +variable "acm_certificate_arn" { + description = "Optional ACM certificate ARN for the ALB. Leave blank to use Secrets Manager." + type = string + default = "" +} + +variable "ssl_policy" { + description = "SSL policy to use for the ALB HTTPS listener" + type = string + # Modern TLS 1.2/1.3 policy; adjust for legacy client support if needed + default = "ELBSecurityPolicy-TLS13-1-2-2021-06" +} + +# ===================================================================== +# ECS Deployment knobs +# ===================================================================== +variable "log_retention_days" { + description = "CloudWatch log retention (days)" + type = number + default = 30 +} + +variable "platform_version" { + description = "Fargate platform version to use" + type = string + default = "LATEST" +} + +variable "deployment_maximum_percent" { + description = "Upper limit of tasks running during deployment" + type = number + default = 200 +} + +variable "deployment_minimum_healthy_percent" { + description = "Lower limit of tasks running during deployment" + type = number + default = 50 +} + +variable "deployment_circuit_breaker_enabled" { + description = "Enable deployment circuit breaker" + type = bool + default = true +} + +variable "deployment_circuit_breaker_rollback" { + description = "Enable rollback if deployment fails" + type = bool + default = true +} + + +variable "environment" { + description = "Short env name (e.g., bo, qa, prod)" + type = string + default = "prod" +} + +variable "alb_deletion_protection" { + description = "Enable deletion protection on the ALB" + type = bool + default = true +} + +variable "waf_allowed_countries" { + description = "List of ISO country codes for WAF (unused unless explicitly wired)" + type = list(string) + default = [] +} + +############################ +# RDS-specific variables +############################ + +variable "db_identifier" { + description = "Unique identifier for the RDS instance." + type = string + default = "mpath-bo-mysql" +} + +variable "db_name" { + description = "Initial database name." + type = string + default = "mpath_prod" +} + +variable "db_username" { + description = "Database master or app username." + type = string + default = "mpath_admin" +} + +variable "db_password" { + description = "Optional DB password. If null, one will be auto-generated." + type = string + default = null + sensitive = true +} + +variable "db_allocated_storage" { + description = "Storage in GiB (gp2 minimum is 20)." + type = number + default = 20 +} + +variable "kms_key_id" { + description = "Optional KMS key for encrypting the secret." + type = string + default = null +} + +variable "secret_name" { + description = "Secrets Manager name for DB credentials bundle." + type = string + default = "mpath/bo/db" +} variable "tags" { - type = map(string) - default = {} + description = "Common tags to apply to RDS resources." + type = map(string) + default = { + App = "mPATH" + Env = "bo" + } } -variable "aws_region" { - default = "us-east-1" -} \ No newline at end of file +variable "ecs_tasks_sg_name" { + description = "Name of the ECS tasks security group to allow into MySQL. If null, uses a convention." + type = string + default = null +} + +# ── Rails Environment ───────────────────────────────────────── +variable "rails_env" { + type = string + default = "production" + description = "Rails environment (e.g., development, staging, production)" +} + +variable "rails_log_to_stdout" { + type = bool + default = true + description = "Enable Rails to log to STDOUT (for ECS/Docker environments)" +} + +variable "rails_serve_static" { + type = bool + default = true + description = "Serve static files directly from Rails" +} + +variable "puma_port" { + type = number + default = 3000 + description = "Port that Puma web server listens on" +} + +variable "web_concurrency" { + type = number + default = 2 + description = "Number of Puma worker processes" +} + +variable "rails_max_threads" { + type = number + default = 5 + description = "Maximum number of threads per Puma worker" +} + +variable "rails_min_threads" { + type = number + default = 2 + description = "Minimum number of threads per Puma worker" +} + +# ── Secrets & Keys ──────────────────────────────────────────── +variable "secret_key_base" { + type = string + default = null + description = "Rails SECRET_KEY_BASE; if null, Terraform generates and stores one in Secrets Manager" +} + +# ── Office 365 (SSO) ───────────────────────────────────────── +variable "office365_client_id" { + type = string + default = "" + description = "Office 365 application client ID" +} + +variable "office365_client_secret" { + type = string + default = "" + description = "Office 365 application client secret" +} + +variable "office365_redirect_uri" { + type = string + default = "" + description = "Redirect URI for Office 365 OAuth2 flow" +} + +variable "office365_provider_url" { + type = string + default = "" + description = "Office 365 provider or authorization endpoint" +} + +# ── Keycloak (SSO) ──────────────────────────────────────────── +variable "keycloak_client_id" { + type = string + default = "your-keycloak-client-id" + description = "Keycloak client ID" +} + +variable "keycloak_client_secret" { + type = string + default = "your-keycloak-client-secret" + description = "Keycloak client secret" +} + +variable "keycloak_realm" { + type = string + default = "your-keycloak-realm" + description = "Keycloak realm name" +} + +variable "keycloak_server_url" { + type = string + default = "https://xx" + description = "Base URL for your Keycloak server" +} + +# ── SSL / HTTPS ────────────────────────────────────────────── +variable "use_ssl" { + type = bool + default = false + description = "Enable SSL (HTTPS) for the application" +} diff --git a/deployment/ecs/main.tf b/deployment/ecs/main.tf index 6bb1ccf23..e5bafa6b9 100644 --- a/deployment/ecs/main.tf +++ b/deployment/ecs/main.tf @@ -1,18 +1,23 @@ terraform { + required_version = ">= 1.13.3" + backend "s3" {} required_providers { - aws = { source = "hashicorp/aws", version = "~> 5.0" } + aws = { + source = "hashicorp/aws" + version = "~> 5.95" # stable; avoids the 5.100.x ARM crash + } } - backend "s3" {} # init with -backend-config=backend-prod.hcl } + provider "aws" { region = var.aws_region } locals { app_name = "mpath" common_tags = merge(var.tags, { - Project = "mpath" + Project = "mpath" Environment = "shared-network" - ManagedBy = "Terraform" + ManagedBy = "Terraform" }) } @@ -21,7 +26,7 @@ resource "aws_vpc" "main" { cidr_block = var.vpc_cidr_block enable_dns_hostnames = true enable_dns_support = true - tags = merge(local.common_tags, { Name = var.vpc_name }) + tags = merge(local.common_tags, { Name = var.vpc_name }) } # IGW @@ -57,8 +62,8 @@ resource "aws_subnet" "private" { # NAT (single-AZ cost saver) resource "aws_eip" "nat" { - domain = "vpc" - tags = merge(local.common_tags, { Name = "${var.nat_gateway_name}-eip" }) + domain = "vpc" + tags = merge(local.common_tags, { Name = "${var.nat_gateway_name}-eip" }) depends_on = [aws_internet_gateway.main] } @@ -72,13 +77,19 @@ resource "aws_nat_gateway" "main" { # Route tables resource "aws_route_table" "public" { vpc_id = aws_vpc.main.id - route { cidr_block = "0.0.0.0/0"; gateway_id = aws_internet_gateway.main.id } + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main.id + } tags = merge(local.common_tags, { Name = "${var.vpc_name}-public-rt" }) } resource "aws_route_table" "private" { vpc_id = aws_vpc.main.id - route { cidr_block = "0.0.0.0/0"; nat_gateway_id = aws_nat_gateway.main.id } + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.main.id + } tags = merge(local.common_tags, { Name = "${var.vpc_name}-private-rt" }) } @@ -94,18 +105,3 @@ resource "aws_route_table_association" "private" { route_table_id = aws_route_table.private.id } -# Outputs (env stacks will read these) -output "vpc_id" { - description = "Shared VPC ID" - value = aws_vpc.main.id -} - -output "private_subnet_ids" { - description = "Private subnet IDs for ECS tasks" - value = aws_subnet.private[*].id -} - -output "public_subnet_ids" { - description = "Public subnet IDs for ALBs" - value = aws_subnet.public[*].id -} diff --git a/deployment/ecs/modules/ecs/main.tf b/deployment/ecs/modules/ecs/main.tf index 0870d1347..14c2a53fc 100644 --- a/deployment/ecs/modules/ecs/main.tf +++ b/deployment/ecs/modules/ecs/main.tf @@ -2,9 +2,33 @@ data "aws_region" "current" {} data "aws_caller_identity" "current" {} + # Toggle ALB creation inside this module locals { do_alb = var.create_alb + + # Use explicit ALB name if provided; else default to "-alb" + alb_name_effective = coalesce(var.alb_name, "${var.service_name}-alb") +} + +# Secrets to inject into the container (built from the ARNs passed in) +locals { + container_secrets = concat( + var.db_secret_arn != null ? [ + { name = "DB_USERNAME", valueFrom = "${var.db_secret_arn}:username::" }, + { name = "DB_PASSWORD", valueFrom = "${var.db_secret_arn}:password::" }, + { name = "DB_HOST", valueFrom = "${var.db_secret_arn}:host::" }, + { name = "DB_PORT", valueFrom = "${var.db_secret_arn}:port::" }, + { name = "DB_NAME", valueFrom = "${var.db_secret_arn}:database::" }, + { name = "DB_ADAPTER", valueFrom = "${var.db_secret_arn}:adapter::" }, + { name = "DATABASE_URL", valueFrom = "${var.db_secret_arn}:url::" } + ] : [], + var.app_secret_arn != null ? [ + # Rails core + { name = "SECRET_KEY_BASE", valueFrom = "${var.app_secret_arn}:SECRET_KEY_BASE::" }, + #{ name = "OFFICE365_CLIENT_ID", valueFrom = "${var.app_secret_arn}:OFFICE365_CLIENT_ID::" } + ] : [] + ) } resource "aws_ecs_cluster" "main" { @@ -54,11 +78,13 @@ resource "aws_iam_role" "ecs_task_role" { tags = var.tags } +# AWS-managed policy for pulling from ECR, writing logs, etc. resource "aws_iam_role_policy_attachment" "ecs_execution_role_policy" { role = aws_iam_role.ecs_execution_role.name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } +# Extra ECR actions (kept from your original) resource "aws_iam_role_policy" "ecs_execution_role_ecr_policy" { name = "${var.service_name}-ecs-execution-ecr-policy" role = aws_iam_role.ecs_execution_role.id @@ -78,6 +104,7 @@ resource "aws_iam_role_policy" "ecs_execution_role_ecr_policy" { }) } +# ---- ECS Task Definition (inject secrets) ---- resource "aws_ecs_task_definition" "app" { family = var.service_name requires_compatibilities = ["FARGATE"] @@ -95,7 +122,7 @@ resource "aws_ecs_task_definition" "app" { portMappings = [ { - containerPort = var.container_port # set to 8443 in env + containerPort = var.container_port protocol = "tcp" } ] @@ -107,6 +134,14 @@ resource "aws_ecs_task_definition" "app" { } ] + # Secrets injected by ECS at container start + secrets = local.container_secrets + # Run Rails seeds after short delay, then start the app + command = [ + "bash", "-lc", + "sleep 10 && echo 'Running Rails seeds...' && bundle exec rails db:seed RAILS_ENV=production || true; echo 'Starting Puma...' && bundle exec puma -C config/puma.rb" + ] + logConfiguration = { logDriver = "awslogs", options = { @@ -115,6 +150,7 @@ resource "aws_ecs_task_definition" "app" { awslogs-stream-prefix = "ecs" } } + healthCheck = var.health_check_enabled ? { command = ["CMD-SHELL", "curl -f http://localhost:${var.container_port}${var.health_check_path} || exit 1"] @@ -130,6 +166,7 @@ resource "aws_ecs_task_definition" "app" { } +# ---- Networking SGs ---- resource "aws_security_group" "ecs_service" { name_prefix = "${var.service_name}-ecs-" vpc_id = var.vpc_id @@ -143,7 +180,7 @@ resource "aws_security_group" "ecs_service" { to_port = var.container_port protocol = "tcp" security_groups = [ingress.value] - description = "Allowed SG -> ECS container port" + description = "Allowed SG to ECS container port" } } @@ -155,12 +192,10 @@ resource "aws_security_group" "ecs_service" { to_port = var.container_port protocol = "tcp" security_groups = [ingress.value] - description = "Module ALB SG -> ECS container port" + description = "Module ALB SG to ECS container port" } } - - egress { from_port = 0 to_port = 0 @@ -173,7 +208,6 @@ resource "aws_security_group" "ecs_service" { lifecycle { create_before_destroy = true } } - # ALB SG resource "aws_security_group" "alb" { count = local.do_alb ? 1 : 0 @@ -181,28 +215,45 @@ resource "aws_security_group" "alb" { vpc_id = var.vpc_id description = "ALB SG for ${var.service_name}" - ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } - ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } - egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } + ingress { + description = "HTTP" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "HTTPS" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } tags = merge(var.tags, { Name = "${var.service_name}-alb-sg" }) } # ALB resource "aws_lb" "this" { - count = local.do_alb ? 1 : 0 - name = coalesce(var.alb_name, "${var.service_name}-alb") - internal = false - load_balancer_type = "application" - security_groups = local.do_alb ? [aws_security_group.alb[0].id] : null - subnets = local.do_alb ? var.public_subnet_ids : null + count = local.do_alb ? 1 : 0 + name = local.alb_name_effective + internal = false + load_balancer_type = "application" + security_groups = local.do_alb ? [aws_security_group.alb[0].id] : null + subnets = local.do_alb ? var.public_subnet_ids : null enable_deletion_protection = var.alb_deletion_protection - tags = var.tags + tags = var.tags } # Target Group (ALB -> ECS tasks) -# Note: default to HTTP to the container on var.container_port (8443). -# If your app speaks TLS on 8443, change protocol to "HTTPS" and add a cert on the task or use TLS termination differently. resource "aws_lb_target_group" "ecs" { count = local.do_alb ? 1 : 0 name = "${var.service_name}-tg" @@ -239,8 +290,6 @@ resource "aws_lb_listener" "https" { type = "forward" target_group_arn = aws_lb_target_group.ecs[0].arn } - - tags = var.tags } # HTTP -> HTTPS redirect @@ -258,14 +307,10 @@ resource "aws_lb_listener" "http" { status_code = "HTTP_301" } } - - tags = var.tags } - locals { - effective_tg_arn = var.target_group_arn != null ? var.target_group_arn : - (local.do_alb ? aws_lb_target_group.ecs[0].arn : null) + effective_tg_arn = var.target_group_arn != null ? var.target_group_arn : (local.do_alb ? aws_lb_target_group.ecs[0].arn : null) } resource "aws_ecs_service" "app" { @@ -290,7 +335,6 @@ resource "aws_ecs_service" "app" { rollback = var.deployment_circuit_breaker_rollback } - # Attach to TG if provided/created dynamic "load_balancer" { for_each = local.effective_tg_arn != null ? [1] : [] content { @@ -306,3 +350,40 @@ resource "aws_ecs_service" "app" { aws_iam_role_policy_attachment.ecs_execution_role_policy ] } + +# ---- Allow the ECS *execution role* to read Secrets Manager (and KMS if needed) ---- +locals { + secret_arns = compact([var.db_secret_arn, var.app_secret_arn]) + kms_key_arns_ = var.kms_key_arns # optional list; pass only if you used CMKs on the secrets +} + +resource "aws_iam_role_policy" "ecs_exec_secrets" { + count = length(local.secret_arns) > 0 ? 1 : 0 + name = "${var.service_name}-exec-secrets" + role = aws_iam_role.ecs_execution_role.name + + policy = jsonencode({ + Version = "2012-10-17", + Statement = concat( + [ + { + Sid = "ReadSecretsFromSecretsManager" + Effect = "Allow" + Action = [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ] + Resource = local.secret_arns + } + ], + length(local.kms_key_arns_) > 0 ? [ + { + Sid = "DecryptSecretsWithKMS" + Effect = "Allow" + Action = ["kms:Decrypt"] + Resource = local.kms_key_arns_ + } + ] : [] + ) + }) +} diff --git a/deployment/ecs/modules/ecs/outputs.tf b/deployment/ecs/modules/ecs/outputs.tf index 7dfc0acb7..f515f3592 100644 --- a/deployment/ecs/modules/ecs/outputs.tf +++ b/deployment/ecs/modules/ecs/outputs.tf @@ -89,3 +89,8 @@ output "target_group_arn_effective" { description = "Target Group ARN actually used by the ECS service" value = local.effective_tg_arn } + +output "service_sg_id" { + description = "Security Group ID attached to the ECS service/tasks" + value = aws_security_group.ecs_service.id +} diff --git a/deployment/ecs/modules/ecs/variables.tf b/deployment/ecs/modules/ecs/variables.tf index 7dfa4fc25..98ebbcbaf 100644 --- a/deployment/ecs/modules/ecs/variables.tf +++ b/deployment/ecs/modules/ecs/variables.tf @@ -16,7 +16,7 @@ variable "container_image" { variable "container_port" { description = "Port the container exposes" type = number - default = 8443 + default = 8443 } variable "desired_count" { @@ -123,6 +123,12 @@ variable "create_alb" { description = "Create ALB/TG/listeners inside the module" type = bool default = false + +validation { + condition = !var.create_alb || (length(var.public_subnet_ids) > 0 && length(trimspace(var.acm_certificate_arn)) > 0) + error_message = "When create_alb = true, you must provide public_subnet_ids and acm_certificate_arn." +} + } variable "public_subnet_ids" { @@ -140,11 +146,12 @@ variable "acm_certificate_arn" { variable "ssl_policy" { description = "TLS policy for HTTPS listener" type = string + # Consider: "ELBSecurityPolicy-TLS13-1-2-2021-06" for modern clients default = "ELBSecurityPolicy-TLS-1-2-2017-01" } variable "alb_name" { - description = "Name for the ALB (defaults to ${service_name}-alb)" + description = "Optional explicit ALB name; if null, the module will use -alb." type = string default = null } @@ -161,8 +168,25 @@ variable "allowed_source_sg_ids" { default = [] } +variable "target_group_arn" { + description = "Optional existing Target Group ARN to attach ECS service to; if null, the module-created TG is used." + type = string + default = null +} -validation { - condition = !(var.create_alb) || (length(var.public_subnet_ids) > 0 && length(var.acm_certificate_arn) > 0) - error_message = "When create_alb = true, you must provide public_subnet_ids and acm_certificate_arn." +variable "db_secret_arn" { + type = string + default = null +} + +variable "app_secret_arn" { + type = string + default = null +} + + +variable "kms_key_arns" { + type = list(string) + default = [] + description = "Optional list of KMS key ARNs used to encrypt the secrets; grants kms:Decrypt to the execution role." } diff --git a/deployment/ecs/outputs.tf b/deployment/ecs/outputs.tf index 5c3cf77fd..b97b537ab 100644 --- a/deployment/ecs/outputs.tf +++ b/deployment/ecs/outputs.tf @@ -1,54 +1,3 @@ -output "ecs_cluster_name" { - description = "Name of the ECS cluster" - value = module.ecs.cluster_id -} - -output "ecs_service_name" { - description = "Name of the ECS service" - value = module.ecs.service_id -} - -output "task_definition_arn" { - description = "ARN of the ECS task definition" - value = module.ecs.task_definition_arn -} - -output "security_group_id" { - description = "Security group ID for the ECS service" - value = module.ecs.security_group_id -} - -output "log_group_name" { - description = "CloudWatch log group for the application" - value = module.ecs.log_group_name -} - -output "log_group_arn" { - description = "CloudWatch log group ARN for the application" - value = module.ecs.log_group_arn -} - -output "ecs_cluster_name_friendly" { - description = "Friendly name of the ECS cluster" - value = module.ecs.cluster_name -} - -output "ecs_service_name_friendly" { - description = "Friendly name of the ECS service" - value = module.ecs.service_name -} - -output "rds_endpoint" { - description = "RDS instance endpoint" - value = module.rds.db_instance_endpoint - sensitive = true -} - -output "rds_instance_id" { - description = "RDS instance ID" - value = module.rds.db_instance_id -} - output "vpc_id" { description = "ID of the VPC" value = aws_vpc.main.id @@ -64,31 +13,12 @@ output "private_subnet_ids" { value = aws_subnet.private[*].id } -output "alb_dns_name" { - description = "DNS name of the load balancer" - value = aws_lb.mpath_production_alb.dns_name -} - -output "alb_zone_id" { - description = "Zone ID of the load balancer" - value = aws_lb.mpath_production_alb.zone_id -} - -output "alb_arn" { - description = "ARN of the load balancer" - value = aws_lb.mpath_production_alb.arn -} - -output "target_group_arn" { - description = "ARN of the target group" - value = aws_lb_target_group.ecs_tg.arn -} - -# root/outputs.tf output "waf_web_acl_arn" { - value = aws_wafv2_web_acl.mpath_web_acl.arn + description = "ARN of the WAF Web ACL" + value = aws_wafv2_web_acl.mpath_web_acl.arn } output "waf_web_acl_id" { - value = aws_wafv2_web_acl.mpath_web_acl.id + description = "ID of the WAF Web ACL" + value = aws_wafv2_web_acl.mpath_web_acl.id } diff --git a/deployment/ecs/variables.tf b/deployment/ecs/variables.tf index 8b5f9dee9..f444218fc 100644 --- a/deployment/ecs/variables.tf +++ b/deployment/ecs/variables.tf @@ -1,5 +1,5 @@ variable "environment" { - description = "Environment name (e.g., Development, Staging, Production)" + description = "Environment name" type = string default = "Production" @@ -39,12 +39,8 @@ variable "vpc_name" { variable "vpc_cidr_block" { description = "The CIDR block for the VPC" - default = "192.168.29.0/24" -} - -variable "certificate_arn" { - description = "ARN of the ACM certificate for HTTPS listeners" type = string + default = "192.168.29.0/24" } variable "internet_gateway_name" { @@ -69,229 +65,14 @@ variable "nat_gateway_name" { } } -variable "aws_account_id" { - description = "AWS Account ID for ECR repository" - type = string -} - -variable "container_image_tag" { - description = "Container image tag" - type = string - default = "latest" -} - -variable "desired_count" { - description = "Desired number of ECS tasks" - type = number - default = 1 -} - -variable "cpu" { - description = "CPU units for the ECS task" - type = number - default = 512 -} - -variable "memory" { - description = "Memory for the ECS task in MB" - type = number - default = 1024 -} - -variable "database_url" { - description = "Database connection URL" - type = string - sensitive = true -} - -variable "secret_key" { - description = "Application secret key" - type = string - sensitive = true -} - -variable "bw_org_id" { - description = "Bitwarden Organization ID" - type = string - sensitive = true -} - -variable "bw_access_token" { - description = "Bitwarden Access Token" - type = string - sensitive = true -} - -variable "bw_project_id" { - description = "Bitwarden Project ID" - type = string - sensitive = true -} - -variable "postgres_database" { - type = string - description = "The name of the database" - sensitive = true -} - -variable "postgres_username" { - type = string - description = "The username for the database" - sensitive = true -} - -variable "postgres_password" { - type = string - description = "The password for the database" - sensitive = true -} - -variable "identifier" { - type = string - description = "The identifier for the RDS instance" - sensitive = true -} - -variable "instance_class" { - description = "The instance class to use for the RDS instance." - type = string - default = "db.m5.large" -} - -variable "rds_instance_class" { - description = "RDS instance class" - type = string - default = "db.m5.xlarge" - - validation { - condition = can(regex("^db\\.", var.rds_instance_class)) - error_message = "RDS instance class must start with 'db.'" - } -} - -variable "rds_allocated_storage" { - description = "RDS allocated storage in GB" - type = number - default = 250 - - validation { - condition = var.rds_allocated_storage >= 20 && var.rds_allocated_storage <= 65536 - error_message = "RDS allocated storage must be between 20 GB and 65536 GB." - } -} - -# RDS CloudWatch Logging Variables -variable "rds_enabled_cloudwatch_logs_exports" { - description = "List of log types to export to CloudWatch for RDS" - type = list(string) - default = ["postgresql"] -} - -variable "rds_performance_insights_enabled" { - description = "Enable Performance Insights for RDS" - type = bool - default = true -} - -variable "rds_performance_insights_retention_period" { - description = "Amount of time in days to retain Performance Insights data" - type = number - default = 7 -} - -variable "rds_monitoring_interval" { - description = "The interval for collecting enhanced monitoring metrics" - type = number - default = 60 -} - -variable "tags" { - description = "A map of tags to apply to all resources" - type = map(string) - default = {} -} - -variable "ecs_log_retention_days" { - description = "CloudWatch log retention period in days for ECS" - type = number - default = 30 -} - -variable "ecs_health_check_enabled" { - description = "Enable health checks for ECS containers" - type = bool - default = true -} - -variable "ecs_health_check_path" { - description = "Health check path for the application" - type = string - default = "/health" -} - -variable "ecs_assign_public_ip" { - description = "Assign public IP to ECS tasks" - type = bool - default = true -} - -variable "ecs_platform_version" { - description = "ECS platform version" - type = string - default = "LATEST" -} - -variable "ecs_deployment_maximum_percent" { - description = "Upper limit on the number of running tasks during deployment" - type = number - default = 200 -} - -variable "ecs_deployment_minimum_healthy_percent" { - description = "Lower limit on the number of running tasks during deployment" - type = number - default = 50 -} - -variable "ecs_deployment_circuit_breaker_enabled" { - description = "Enable deployment circuit breaker" - type = bool - default = true -} - -variable "ecs_deployment_circuit_breaker_rollback" { - description = "Enable rollback on deployment failure" - type = bool - default = true -} - -variable "ecs_container_insights_enabled" { - description = "Enable container insights for the ECS cluster" - type = bool - default = true -} - -variable "alb_deletion_protection" { - description = "Enable deletion protection for ALB" - type = bool - default = true -} - -variable "alb_ssl_policy" { - description = "SSL policy for HTTPS listener" - type = string - default = "ELBSecurityPolicy-TLS-1-2-2017-01" -} - variable "waf_allowed_countries" { description = "List of allowed country codes for WAF geo restriction" type = list(string) default = ["US"] } -# using these in the name/tags: -variable "environment" { type = string } # e.g., "shared" if this WAF is reused -locals { - app_name = "mpath" # or from a var if you prefer - common_tags = { Project = "mpath", ManagedBy = "Terraform" } +variable "tags" { + description = "A map of tags to apply to all shared resources" + type = map(string) + default = {} } diff --git a/deployment/ecs/waf.tf b/deployment/ecs/waf.tf index efd036347..5db18ecf4 100644 --- a/deployment/ecs/waf.tf +++ b/deployment/ecs/waf.tf @@ -4,22 +4,29 @@ resource "aws_wafv2_web_acl" "mpath_web_acl" { description = "${var.environment} ${local.app_name} WebACL" scope = "REGIONAL" - default_action { allow {} } - + default_action { + allow {} + } + # 1) Allow-only list of countries; everything else is blocked rule { name = "block-non-allowed-countries" priority = 0 + statement { not_statement { statement { geo_match_statement { - country_codes = var.waf_allowed_countries # e.g., ["US"] + country_codes = var.waf_allowed_countries # e.g., ["US"] } } } } - action { block {} } + + action { + block {} + } + visibility_config { sampled_requests_enabled = true cloudwatch_metrics_enabled = true @@ -27,35 +34,47 @@ resource "aws_wafv2_web_acl" "mpath_web_acl" { } } - # 2) Block known bad IPs (very low false-positive) + # 2) AWS managed IP reputation (enforced) rule { name = "AWS-AWSManagedRulesAmazonIpReputationList" priority = 1 + statement { managed_rule_group_statement { - name = "AWSManagedRulesAmazonIpReputationList" vendor_name = "AWS" + name = "AWSManagedRulesAmazonIpReputationList" } } - override_action { none {} } # enforce block + + # 'none' = respect the rule group's native action (block) + override_action { + none {} + } + visibility_config { + sampled_requests_enabled = true cloudwatch_metrics_enabled = true metric_name = "AWS-AWSManagedRulesAmazonIpReputationList" - sampled_requests_enabled = true } } - # 3) Common rules in COUNT mode (observe first, then enforce later) + # 3) Common rules in COUNT mode (observe first) rule { name = "AWS-AWSManagedRulesCommonRuleSet" priority = 2 + statement { managed_rule_group_statement { vendor_name = "AWS" name = "AWSManagedRulesCommonRuleSet" } } - override_action { count {} } # low-risk: no blocking yet + + # 'count' = do not block yet; just record matches + override_action { + count {} + } + visibility_config { sampled_requests_enabled = true cloudwatch_metrics_enabled = true @@ -63,6 +82,7 @@ resource "aws_wafv2_web_acl" "mpath_web_acl" { } } + # Web ACL level visibility visibility_config { sampled_requests_enabled = true cloudwatch_metrics_enabled = true diff --git a/docker-compose.yaml b/docker-compose.yaml index 04de08e83..336c3c611 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,8 +1,5 @@ version: '3.8' -networks: - app_network: - services: app: build: @@ -11,48 +8,13 @@ services: container_name: app restart: always ports: - - "3000:3000" # Expose port 3000 for local access + - "8443:8443" env_file: - .env environment: RAILS_ENV: production - DATABASE_URL: mysql2://mpath_user:mpath_pass@mysql:3306/mpath - DATABASE_HOST: mysql - DATABASE_PORT: 3306 - DATABASE_NAME: mpath - DATABASE_USER: mpath_user - DATABASE_PASSWORD: mpath_pass - PUMA_PORT: 3000 # Match exposed port - RAILS_MAX_THREADS: 5 - RAILS_MIN_THREADS: 5 WEB_CONCURRENCY: 2 PUMA_PIDFILE: /var/www/mPATH/tmp/pids/server.pid - networks: - - app_network volumes: - .:/var/www/mPATH - depends_on: - - mysql entrypoint: ["/bin/bash", "/var/www/mPATH/docker/app/entrypoint.sh"] - - mysql: - build: - context: docker/mysql - dockerfile: Dockerfile - container_name: mysql - environment: - MYSQL_ROOT_PASSWORD: root_pass - MYSQL_DATABASE: mpath - MYSQL_USER: mpath_user - MYSQL_PASSWORD: mpath_pass - ports: - - "3306:3306" - volumes: - - mysql-data:/var/lib/mysql - - ./dump.sql:/docker-entrypoint-initdb.d/dump.sql - networks: - - app_network - restart: always - -volumes: - mysql-data: diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index 83c9a52ee..438b1d07a 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -1,63 +1,110 @@ -# Use the official Ruby image -FROM ruby:3.1.0 +# Builder stage for installing dependencies +FROM ruby:3.1.0 AS builder -# Set the working directory inside the container WORKDIR /var/www/mPATH -# Install system dependencies, including gosu (no Nginx here to keep it clean) +# Install system dependencies (build-essential for native gems like mysql2, Node.js for Webpacker) RUN apt-get update -qq && apt-get install -y \ + build-essential \ curl dirmngr gnupg apt-transport-https ca-certificates \ software-properties-common \ default-mysql-client libmariadb-dev wget \ - && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.16/gosu-amd64" \ - && chmod +x /usr/local/bin/gosu \ - && gosu nobody true \ - && rm -rf /var/lib/apt/lists/* - -# Install Node.js (>=14.15.0) and Yarn -RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ + && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ && apt-get install -y nodejs \ - && npm install -g yarn + && npm install -g yarn \ + && rm -rf /var/lib/apt/lists/* # Install Bundler RUN gem install bundler:2.3.26 -# Create puma user and group -RUN groupadd -r puma && useradd -r -g puma -d /var/www/mPATH puma +# Copy lockfiles for caching +COPY Gemfile Gemfile.lock package.json yarn.lock ./ + +# Bundle config and install (production only) +RUN bundle config set --local without 'development test' \ + && bundle install -# Copy the application source code +# Set production env for JS installs +ENV NODE_ENV=production + +# Install JS dependencies (production only) +RUN yarn install --production --frozen-lockfile + +# Install all Node dependencies +ENV NODE_ENV=production + +RUN npm install + +# Copy full app code COPY . . -# Set entrypoint permissions while still root -RUN chmod +x /var/www/mPATH/docker/app/entrypoint.sh +# Asset compilation stage (mimic runtime environment) +FROM builder AS asset-compiler + +WORKDIR /var/www/mPATH + +# Set production environment to match runtime +ENV RAILS_ENV=production NODE_ENV=production + +# Create puma user and group (needed for permissions in asset compilation) +RUN groupadd -r puma && useradd -r -g puma -d /var/www/mPATH puma + +# Set permissions as root USER root -# Single ownership/permissions pass (no duplicates later) RUN mkdir -p /var/www/mPATH/tmp/pids /var/www/mPATH/tmp/cache /var/www/mPATH/log \ && chown -R puma:puma /var/www/mPATH /usr/local/bundle /tmp \ && chmod -R 755 /tmp -# Switch to the puma user for security +# Switch to puma user for asset compilation (mimics runtime) USER puma -# Bundler config and install (writes into /usr/local/bundle which puma now owns) -RUN bundle config set --local without 'development test' \ - && bundle install +# Install Webpacker/Shakapacker (no-op if already installed) +RUN bundle exec rails webpacker:install || true \ + && bundle exec rails shakapacker:install || true + +# Precompile assets (use dummy secret; ensure config.assets.initialize_on_precompile = false in config/application.rb) +RUN SECRET_KEY_BASE=dummy bundle exec rails assets:clobber assets:precompile + +# Verify manifest.json exists (debugging step) +RUN test -f /var/www/mPATH/public/packs/manifest.json && echo "manifest.json created successfully" || { echo "Error: manifest.json not found"; exit 1; } -# Install JavaScript dependencies -RUN yarn install --silent || true +# Final runtime stage +FROM ruby:3.1.0 -# Ensure Webpacker/Shakapacker is installed (no-op if already present) -RUN NODE_ENV=production RAILS_ENV=production bundle exec rails webpacker:install || true -RUN NODE_ENV=production RAILS_ENV=production bundle exec rails shakapacker:install || true +WORKDIR /var/www/mPATH -# Precompile Rails assets (build-time placeholders only) -RUN SECRET_KEY_BASE=placeholder \ - DATABASE_URL=mysql2://mpath_user:mpath_pass@mysql/mpath \ - RAILS_ENV=production \ - bundle exec rake assets:clobber assets:precompile +# Install minimal runtime dependencies (libmariadb3 for mysql2 runtime) +RUN apt-get update -qq && apt-get install -y \ + curl dirmngr gnupg apt-transport-https ca-certificates \ + software-properties-common \ + default-mysql-client libmariadb3 wget \ + && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g yarn \ + && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.16/gosu-amd64" \ + && chmod +x /usr/local/bin/gosu \ + && gosu nobody true \ + && rm -rf /var/lib/apt/lists/* + +# Create puma user and group +RUN groupadd -r puma && useradd -r -g puma -d /var/www/mPATH puma + +# Copy bundled gems, app code, and precompiled assets from asset-compiler stage +COPY --from=asset-compiler --chown=puma:puma /usr/local/bundle /usr/local/bundle +COPY --from=asset-compiler --chown=puma:puma /var/www/mPATH /var/www/mPATH + +# Set permissions as root +USER root +RUN chmod +x /var/www/mPATH/docker/app/entrypoint.sh \ + && mkdir -p /var/www/mPATH/tmp/pids /var/www/mPATH/tmp/cache /var/www/mPATH/log \ + && chown -R puma:puma /var/www/mPATH /usr/local/bundle /tmp \ + && chmod -R 755 /tmp + +# Switch to puma user +USER puma -# Expose port for Puma (plain HTTP; TLS terminates at ALB) +# Expose port for Puma EXPOSE 8443 # Entrypoint -ENTRYPOINT ["/bin/bash", "/var/www/mPATH/docker/app/entrypoint.sh"] +ENTRYPOINT ["/bin/bash", "/var/www/mPATH/docker/app/entrypoint.sh"] \ No newline at end of file diff --git a/docker/app/entrypoint.sh b/docker/app/entrypoint.sh index f86aa3cfa..a1ea3ae79 100755 --- a/docker/app/entrypoint.sh +++ b/docker/app/entrypoint.sh @@ -7,15 +7,6 @@ PORT="${PORT:-8443}" echo "Starting mPATH (entrypoint) ..." cd "$APP_HOME" -# Require secrets (do not silently generate in prod) -if [[ -z "${SECRET_KEY_BASE:-}" ]]; then - echo "ERROR: SECRET_KEY_BASE is not set." - exit 1 -fi -if [[ -z "${RAILS_MASTER_KEY:-}" && ! -f "config/master.key" ]]; then - echo "ERROR: RAILS_MASTER_KEY not set and config/master.key not present." - exit 1 -fi # Optional: wait for DB (Lightsail) if requested if [[ "${DB_WAIT:-0}" == "1" ]]; then diff --git a/rebuild_docker.sh b/rebuild_docker.sh index f165019ad..7dee03208 100644 --- a/rebuild_docker.sh +++ b/rebuild_docker.sh @@ -1,16 +1,6 @@ #!/bin/bash -echo "Stopping and Removing Containers, Networks, and Volumes..." -docker compose down -v --remove-orphans +echo "Rebuilding" +docker compose build --no-cache -echo "Pruning Docker system (images, containers, volumes)..." -docker system prune -a --volumes -f - -echo "Removing old MySQL data and temporary files..." -rm -rf docker/mysql/data -rm -rf tmp/pids/server.pid - -echo "Rebuilding and Starting Docker Containers..." -docker compose build --no-cache && docker compose up -d - -echo "rebuild is done" +echo "Rebuild is done"