From 383a44b8362b94706d048f25f485a0d5d611513e Mon Sep 17 00:00:00 2001 From: Joseph Yousefpour Date: Tue, 23 Sep 2025 16:25:02 -0400 Subject: [PATCH 1/5] adding initial dockerfile for mPATH --- .github/workflows/ecr-publish.yml | 1 + config/puma.rb | 58 ++++++++------------- docker/app/Dockerfile | 53 +++++++++---------- docker/app/entrypoint.sh | 86 +++++++++++++++---------------- 4 files changed, 89 insertions(+), 109 deletions(-) diff --git a/.github/workflows/ecr-publish.yml b/.github/workflows/ecr-publish.yml index da628ac12..89ee44aca 100644 --- a/.github/workflows/ecr-publish.yml +++ b/.github/workflows/ecr-publish.yml @@ -4,6 +4,7 @@ on: push: branches: - 'main' + - 'integration' jobs: ecr-publish: diff --git a/config/puma.rb b/config/puma.rb index 085b2c299..7dbe8668e 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,49 +1,33 @@ -require 'yaml' +# frozen_string_literal: true +require "yaml" # Load optional YAML config -if File.exist?('./puma_config.yml') - yaml_data = YAML.load_file('./puma_config.yml') - yaml_data.each do |key, value| - ENV[key] = value if value - end +if File.exist?("./puma_config.yml") + yaml_data = YAML.safe_load(File.read("./puma_config.yml")) || {} + yaml_data.each { |k, v| ENV[k.to_s] = v.to_s if v } end -rails_env = ENV.fetch('RAILS_ENV') -puma_port = Integer(ENV.fetch('PUMA_PORT')) +rails_env = ENV.fetch("RAILS_ENV", "production") +# Support either PUMA_PORT or PORT, fallback to 8443 +puma_port = Integer(ENV["PUMA_PORT"] || ENV["PORT"] || 8443) -max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) -min_threads_count = ENV.fetch('RAILS_MIN_THREADS', max_threads_count) +max_threads_count = Integer(ENV.fetch("RAILS_MAX_THREADS", 5)) +min_threads_count = Integer(ENV.fetch("RAILS_MIN_THREADS", max_threads_count)) -workers Integer(ENV.fetch('WEB_CONCURRENCY', 2)) +workers Integer(ENV.fetch("WEB_CONCURRENCY", 2)) threads min_threads_count, max_threads_count environment rails_env -# Optional logging for debug -puts "[Puma] ENV PUMA_PORT=#{puma_port}" -puts "[Puma] ENV RAILS_ENV=#{rails_env}" - -if rails_env == 'production' - pidfile ENV.fetch('PUMA_PIDFILE') - - # Commented out SSL bind since Nginx handles TLS - # begin - # ssl_bind( - # ENV.fetch('PUMA_SSL_HOST'), - # puma_port, - # key: ENV.fetch('PUMA_SSL_KEY_FILE'), - # cert: ENV.fetch('PUMA_SSL_CERT_FILE'), - # verify_mode: ENV.fetch('PUMA_SSL_VERIFY_MODE') - # ) - # puts "[Puma] SSL bind successful on #{ENV['PUMA_SSL_HOST']}:#{puma_port}" - # rescue KeyError => e - # warn "[Puma] Missing SSL ENV variable: #{e.message}" - # exit 1 - # end - - port puma_port -else - port puma_port -end + +STDOUT.puts "[Puma] ENV PORT=#{ENV['PORT']}" +STDOUT.puts "[Puma] ENV PUMA_PORT=#{ENV['PUMA_PORT']}" +STDOUT.puts "[Puma] RAILS_ENV=#{rails_env}" + +# PID file +pidfile ENV.fetch("PUMA_PIDFILE", File.join(Dir.pwd, "tmp/pids/server.pid")) + +# We’re terminating TLS at ALB +bind "tcp://0.0.0.0:#{puma_port}" plugin :tmp_restart diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile index b2446e790..83c9a52ee 100644 --- a/docker/app/Dockerfile +++ b/docker/app/Dockerfile @@ -4,20 +4,20 @@ FROM ruby:3.1.0 # Set the working directory inside the container WORKDIR /var/www/mPATH -# Install system dependencies, including gosu +# Install system dependencies, including gosu (no Nginx here to keep it clean) RUN apt-get update -qq && apt-get install -y \ curl dirmngr gnupg apt-transport-https ca-certificates \ software-properties-common \ - default-mysql-client libmariadb-dev nginx 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/* + 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 - && \ - apt-get install -y nodejs && \ - npm install -g yarn +RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g yarn # Install Bundler RUN gem install bundler:2.3.26 @@ -28,41 +28,36 @@ RUN groupadd -r puma && useradd -r -g puma -d /var/www/mPATH puma # Copy the application source code COPY . . -# Set correct permissions on certs and fix ownership in one layer -RUN chown -R puma:puma /var/www/mPATH /usr/local/bundle && \ - chown -R puma:puma /tmp && chmod -R 755 /tmp - +# Set entrypoint permissions while still root +RUN chmod +x /var/www/mPATH/docker/app/entrypoint.sh +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 USER puma -# Set Bundler configuration before installing gems -RUN bundle config set --local without 'development test' && \ - bundle install +# Bundler config and install (writes into /usr/local/bundle which puma now owns) +RUN bundle config set --local without 'development test' \ + && bundle install # Install JavaScript dependencies -RUN yarn install --silent +RUN yarn install --silent || true -# Ensure Webpacker/Shakapacker is installed +# 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 -# Precompile Rails assets (including mh_logo.png) +# 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 - -# Switch back to root for final setup -USER root - -# Final chown to ensure correct ownership -RUN chown -R puma:puma /var/www/mPATH /usr/local/bundle - -# Expose port for Puma +# Expose port for Puma (plain HTTP; TLS terminates at ALB) EXPOSE 8443 -# Set the entrypoint script with correct permissions -RUN chmod +x /var/www/mPATH/docker/app/entrypoint.sh +# Entrypoint ENTRYPOINT ["/bin/bash", "/var/www/mPATH/docker/app/entrypoint.sh"] diff --git a/docker/app/entrypoint.sh b/docker/app/entrypoint.sh index c73f97b4a..f86aa3cfa 100755 --- a/docker/app/entrypoint.sh +++ b/docker/app/entrypoint.sh @@ -1,54 +1,54 @@ -#!/bin/bash -set -e +#!/usr/bin/env bash +set -euo pipefail -echo "Starting application setup..." +APP_HOME="/var/www/mPATH" +PORT="${PORT:-8443}" -# Ensure we are in the correct directory -if [ -d "/var/www/mPATH" ]; then - cd /var/www/mPATH -elif [ -d "/app" ]; then - cd /app -else - echo "App directory not found! Exiting..." +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 - -# Ensure SECRET_KEY_BASE is set -if [ -z "$SECRET_KEY_BASE" ]; then - echo "Generating SECRET_KEY_BASE..." - export SECRET_KEY_BASE=$(ruby -e 'require "securerandom"; puts SecureRandom.hex(64)') - if [ -f ".env" ]; then - echo "SECRET_KEY_BASE=$SECRET_KEY_BASE" >> .env - 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 -# Install any missing gems -echo "Installing missing gems..." -bundle check || bundle install - -# Run database migrations -echo "Running database migrations..." -bundle exec rake db:migrate RAILS_ENV=$RAILS_ENV - -# Seed the database (optional: only in non-local environments if needed) -echo "Seeding database..." -bundle exec rake db:seed RAILS_ENV=$RAILS_ENV +# Optional: wait for DB (Lightsail) if requested +if [[ "${DB_WAIT:-0}" == "1" ]]; then + echo "Waiting for database (up to ${DB_WAIT_TIMEOUT:-60}s)..." + end=$((SECONDS + ${DB_WAIT_TIMEOUT:-60})) + while ! bundle exec ruby -e 'require "uri"; require "mysql2"; + u=URI(ENV["DATABASE_URL"]); + Mysql2::Client.new(host:u.host,port:(u.port||3306),username:u.user,password:u.password).close' >/dev/null 2>&1; do + [[ $SECONDS -ge $end ]] && { echo "DB not reachable in time"; exit 1; } + sleep 2 + done +fi -# Precompile assets if not already present -if ! ls public/packs/manifest.json &>/dev/null; then - echo "Precompiling assets..." - bundle exec rake assets:clobber - bundle exec rake assets:precompile RAILS_ENV=$RAILS_ENV +# Migrations (skip by default on steady-state ECS tasks) +if [[ "${SKIP_MIGRATIONS:-1}" != "1" ]]; then + echo "Running migrations..." + bundle exec rails db:migrate else - echo "Assets already precompiled." + echo "Skipping migrations (SKIP_MIGRATIONS=${SKIP_MIGRATIONS:-1})." fi -# Skip chown to avoid permission errors on mounted volumes -echo "Skipping chown: mounted volumes may restrict ownership change" - -# Still try to apply basic permissions, ignoring errors -chmod -R u+rwX,g+rwX,o-rwx /var/www/mPATH || echo "Warning: chmod failed" +# Seeds only when explicitly requested +if [[ "${RUN_SEEDS:-0}" == "1" ]]; then + echo "Seeding database (RUN_SEEDS=1)..." + bundle exec rails db:seed +else + echo "Skipping seeds." +fi -# Start Puma server as puma user -echo "Starting Puma server..." -exec gosu puma bin/rails server -b 0.0.0.0 -p $PUMA_PORT +echo "Booting Puma on 0.0.0.0:${PORT} (HTTP; TLS at ALB)..." +# We're already USER puma; gosu path is here only if this ever runs as root +if [[ "$(id -u)" -eq 0 ]]; then + exec gosu puma:puma bundle exec puma -C config/puma.rb -b tcp://0.0.0.0:${PORT} +else + exec bundle exec puma -C config/puma.rb +fi From 43ccdd4f5749eef34a38823b9c95354adecabd08 Mon Sep 17 00:00:00 2001 From: Joseph Yousefpour Date: Wed, 24 Sep 2025 18:35:50 -0400 Subject: [PATCH 2/5] initial iac --- deployment/deployment.drawio | 94 +++++++ deployment/ecs/backend-prod.hcl | 6 +- deployment/ecs/envs/bo/backend.hcl | 5 + deployment/ecs/envs/bo/main.tf | 88 +++++++ deployment/ecs/envs/bo/terraform.tfvars | 26 ++ deployment/ecs/envs/bo/variables.tf | 26 ++ deployment/ecs/main.tf | 330 +++--------------------- deployment/ecs/modules/ecs/main.tf | 314 ++++++++++++++-------- deployment/ecs/modules/ecs/outputs.tf | 48 +++- deployment/ecs/modules/ecs/variables.tf | 54 +++- deployment/ecs/outputs.tf | 6 +- deployment/ecs/terraform.tfvars | 7 +- deployment/ecs/variables.tf | 6 +- deployment/ecs/waf.tf | 8 +- 14 files changed, 580 insertions(+), 438 deletions(-) create mode 100644 deployment/deployment.drawio create mode 100644 deployment/ecs/envs/bo/backend.hcl create mode 100644 deployment/ecs/envs/bo/main.tf create mode 100644 deployment/ecs/envs/bo/terraform.tfvars create mode 100644 deployment/ecs/envs/bo/variables.tf diff --git a/deployment/deployment.drawio b/deployment/deployment.drawio new file mode 100644 index 000000000..a1730df71 --- /dev/null +++ b/deployment/deployment.drawio @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/deployment/ecs/backend-prod.hcl b/deployment/ecs/backend-prod.hcl index b4c509bea..87355e40b 100644 --- a/deployment/ecs/backend-prod.hcl +++ b/deployment/ecs/backend-prod.hcl @@ -1,5 +1,5 @@ -bucket = "healthmetricsai-pub-terraform-state" -key = "state/terraform.tfstate" +bucket = "mpath-terraform-state" +key = "mpath/ecs/vpc/terraform.tfstate" region = "us-east-1" encrypt = true -dynamodb_table = "healthmetricsai-pub-terraform-state" \ No newline at end of file +use_lockfile = true \ No newline at end of file diff --git a/deployment/ecs/envs/bo/backend.hcl b/deployment/ecs/envs/bo/backend.hcl new file mode 100644 index 000000000..2a44389ee --- /dev/null +++ b/deployment/ecs/envs/bo/backend.hcl @@ -0,0 +1,5 @@ +bucket = "mpath-terraform-state" +key = "mpath/ecs/bo/terraform.tfstate" +region = "us-east-1" +encrypt = true +use_lockfile = true diff --git a/deployment/ecs/envs/bo/main.tf b/deployment/ecs/envs/bo/main.tf new file mode 100644 index 000000000..8cd8e89e4 --- /dev/null +++ b/deployment/ecs/envs/bo/main.tf @@ -0,0 +1,88 @@ +terraform { + required_providers { + aws = { source = "hashicorp/aws", version = "~> 5.0" } + } + backend "s3" {} # init with: terraform init -backend-config=backend.hcl +} + +provider "aws" { region = var.aws_region } + +locals { + app_name = "mpath" + env = "bo" +} + +# Pull shared network from the ROOT stack +data "terraform_remote_state" "root" { + backend = "s3" + config = { + bucket = "YOUR_TF_STATE_BUCKET" + key = "mpath/root/terraform.tfstate" # <-- update to your actual root state key + region = "us-east-1" + dynamodb_table = "YOUR_TF_LOCKS_TABLE" + encrypt = true + } +} + +locals { + vpc_id = data.terraform_remote_state.root.outputs.vpc_id + private_subnet_ids = data.terraform_remote_state.root.outputs.private_subnet_ids + public_subnet_ids = data.terraform_remote_state.root.outputs.public_subnet_ids + + tags = { + Project = local.app_name + Environment = local.env + ManagedBy = "Terraform" + } +} + +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 + + # Container / service + container_image = var.container_image + container_port = var.container_port # set to 8443 in terraform.tfvars + 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_deletion_protection = true + acm_certificate_arn = var.acm_certificate_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 + + 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 +} diff --git a/deployment/ecs/envs/bo/terraform.tfvars b/deployment/ecs/envs/bo/terraform.tfvars new file mode 100644 index 000000000..4dac85ec6 --- /dev/null +++ b/deployment/ecs/envs/bo/terraform.tfvars @@ -0,0 +1,26 @@ +aws_region = "us-east-1" + +# Container +container_image = "295669632222.dkr.ecr.us-east-1.amazonaws.com/microhealthllc/mpath-bo:latest" +container_port = 8443 +desired_count = 2 +cpu = 512 +memory = 1024 +health_check_path = "/health" + +# TLS for ALB +acm_certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" + +# Ops / deployments +log_retention_days = 30 +platform_version = "LATEST" +deployment_maximum_percent = 200 +deployment_minimum_healthy_percent = 50 +deployment_circuit_breaker_enabled = true +deployment_circuit_breaker_rollback= true + +# Optional tags +tags = { + Owner = "Microhealth_Platform" +} diff --git a/deployment/ecs/envs/bo/variables.tf b/deployment/ecs/envs/bo/variables.tf new file mode 100644 index 000000000..15fcc34c5 --- /dev/null +++ b/deployment/ecs/envs/bo/variables.tf @@ -0,0 +1,26 @@ +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 } + +variable "tags" { + type = map(string) + default = {} +} diff --git a/deployment/ecs/main.tf b/deployment/ecs/main.tf index c24c9499b..6bb1ccf23 100644 --- a/deployment/ecs/main.tf +++ b/deployment/ecs/main.tf @@ -1,27 +1,18 @@ terraform { required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.0" - } - } - backend "s3" { - # Reference backend-prod.hcl (Command: terraform init -backend-config=backend-prod.hcl) + aws = { source = "hashicorp/aws", version = "~> 5.0" } } + backend "s3" {} # init with -backend-config=backend-prod.hcl } -provider "aws" { - region = var.aws_region -} +provider "aws" { region = var.aws_region } -# Local values locals { - app_name = "healthmetricsai" - + app_name = "mpath" common_tags = merge(var.tags, { - Project = "HealthMetricsAI" - Environment = var.environment - ManagedBy = "Terraform" + Project = "mpath" + Environment = "shared-network" + ManagedBy = "Terraform" }) } @@ -30,346 +21,91 @@ 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 }) } -# Internet Gateway +# IGW resource "aws_internet_gateway" "main" { vpc_id = aws_vpc.main.id - - tags = merge(local.common_tags, { - Name = var.internet_gateway_name - }) + tags = merge(local.common_tags, { Name = var.internet_gateway_name }) } -# Public Subnets +# Public subnets resource "aws_subnet" "public" { count = length(var.availability_zones) vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.vpc_cidr_block, 4, count.index) availability_zone = var.availability_zones[count.index] map_public_ip_on_launch = true - tags = merge(local.common_tags, { Name = "${var.vpc_name}-public-${count.index + 1}" - Type = "Public" + Tier = "public" }) } -# Private Subnets +# Private subnets resource "aws_subnet" "private" { count = length(var.availability_zones) vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.vpc_cidr_block, 4, count.index + 10) availability_zone = var.availability_zones[count.index] - tags = merge(local.common_tags, { Name = "${var.vpc_name}-private-${count.index + 1}" - Type = "Private" + Tier = "private" }) } -# Elastic IP for NAT Gateway +# NAT (single-AZ cost saver) resource "aws_eip" "nat" { domain = "vpc" - - tags = merge(local.common_tags, { - Name = "${var.nat_gateway_name}-eip" - }) - + tags = merge(local.common_tags, { Name = "${var.nat_gateway_name}-eip" }) depends_on = [aws_internet_gateway.main] } -# NAT Gateway resource "aws_nat_gateway" "main" { allocation_id = aws_eip.nat.id subnet_id = aws_subnet.public[0].id - - tags = merge(local.common_tags, { - Name = var.nat_gateway_name - }) - - depends_on = [aws_internet_gateway.main] + tags = merge(local.common_tags, { Name = var.nat_gateway_name }) + depends_on = [aws_internet_gateway.main] } -# Route Table for Public Subnets +# 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 - } - - tags = merge(local.common_tags, { - Name = "${var.vpc_name}-public-rt" - }) + 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" }) } -# Route Table for Private Subnets 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 - } - - tags = merge(local.common_tags, { - Name = "${var.vpc_name}-private-rt" - }) + 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" }) } -# Associate Public Subnets with Public Route Table resource "aws_route_table_association" "public" { count = length(aws_subnet.public) subnet_id = aws_subnet.public[count.index].id route_table_id = aws_route_table.public.id } -# Associate Private Subnets with Private Route Table resource "aws_route_table_association" "private" { count = length(aws_subnet.private) subnet_id = aws_subnet.private[count.index].id route_table_id = aws_route_table.private.id } -# Security Group for RDS -resource "aws_security_group" "rds" { - name_prefix = "${local.app_name}-rds-" - vpc_id = aws_vpc.main.id - description = "Security group for RDS database" - - ingress { - from_port = 5432 - to_port = 5432 - protocol = "tcp" - security_groups = [module.ecs.security_group_id] - description = "PostgreSQL access from ECS" - } - - # Add ingress rule for private subnet CIDR blocks as backup - ingress { - from_port = 5432 - to_port = 5432 - protocol = "tcp" - cidr_blocks = [for subnet in aws_subnet.private : subnet.cidr_block] - description = "PostgreSQL access from private subnets" - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - description = "All outbound traffic" - } - - tags = merge(local.common_tags, { - Name = "${local.app_name}-rds-sg" - }) - - lifecycle { - create_before_destroy = true - } -} - -# Security Group for ALB -resource "aws_security_group" "alb" { - name_prefix = "${local.app_name}-alb-" - vpc_id = aws_vpc.main.id - description = "Security group for Application Load Balancer" - - ingress { - from_port = 443 - to_port = 443 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - description = "HTTPS access from internet" - } - - ingress { - from_port = 80 - to_port = 80 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - description = "HTTP access from internet (redirects to HTTPS)" - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - description = "All outbound traffic" - } - - tags = merge(local.common_tags, { - Name = "${local.app_name}-alb-sg" - }) - - lifecycle { - create_before_destroy = true - } -} - -# Update ECS Security Group to allow ALB access -resource "aws_security_group_rule" "ecs_alb_ingress" { - type = "ingress" - from_port = 7860 - to_port = 7860 - protocol = "tcp" - source_security_group_id = aws_security_group.alb.id - security_group_id = module.ecs.security_group_id - description = "Allow ALB to access ECS service" -} - -# Application Load Balancer -resource "aws_lb" "healthmetricsai_production_alb" { - name = "${local.app_name}-${var.environment}-alb" - internal = false - load_balancer_type = "application" - security_groups = [aws_security_group.alb.id] - subnets = aws_subnet.public[*].id - - enable_deletion_protection = var.alb_deletion_protection - - tags = merge(local.common_tags, { - Name = "${local.app_name}-${var.environment}-alb" - }) -} - -# Target Group for ECS Service -resource "aws_lb_target_group" "ecs_tg" { - name = "${local.app_name}-${var.environment}-tg" - port = 7860 - protocol = "HTTP" - vpc_id = aws_vpc.main.id - target_type = "ip" - - health_check { - enabled = true - healthy_threshold = 2 - unhealthy_threshold = 2 - timeout = 5 - interval = 30 - path = var.ecs_health_check_path - matcher = "200" - port = "traffic-port" - protocol = "HTTP" - } - - tags = merge(local.common_tags, { - Name = "${local.app_name}-${var.environment}-tg" - }) -} - -# HTTPS Listener -resource "aws_lb_listener" "https" { - load_balancer_arn = aws_lb.healthmetricsai_production_alb.arn - port = "443" - protocol = "HTTPS" - ssl_policy = var.alb_ssl_policy - certificate_arn = var.certificate_arn - - default_action { - type = "forward" - - forward { - target_group { - arn = aws_lb_target_group.ecs_tg.arn - } - } - } - - tags = local.common_tags -} - -# HTTP Listener (redirect to HTTPS) -resource "aws_lb_listener" "http" { - load_balancer_arn = aws_lb.healthmetricsai_production_alb.arn - port = "80" - protocol = "HTTP" - - default_action { - type = "redirect" - - redirect { - port = "443" - protocol = "HTTPS" - status_code = "HTTP_301" - } - } - - tags = local.common_tags +# Outputs (env stacks will read these) +output "vpc_id" { + description = "Shared VPC ID" + value = aws_vpc.main.id } -# ECS Deployment -module "ecs" { - source = "./modules/ecs" - - cluster_name = "${local.app_name}-${var.environment}" - service_name = "${local.app_name}-service" - container_image = "${var.aws_account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/microhealthllc/healthmetricsai:${var.container_image_tag}" - container_port = 7860 - desired_count = var.desired_count - cpu = var.cpu - memory = var.memory - - vpc_id = aws_vpc.main.id - subnet_ids = aws_subnet.private[*].id # Changed to private subnets - - environment_variables = { - DATABASE_URL = "postgresql://${var.postgres_username}:${var.postgres_password}@${module.rds.db_instance_endpoint}/${var.postgres_database}" - SECRET_KEY = var.secret_key - BW_ORG_ID = var.bw_org_id - BW_ACCESS_TOKEN = var.bw_access_token - BW_PROJECT_ID = var.bw_project_id - } - - # ECS Configuration Variables - log_retention_days = var.ecs_log_retention_days - health_check_enabled = var.ecs_health_check_enabled - health_check_path = var.ecs_health_check_path - assign_public_ip = false # Changed to false since we're in private subnets - platform_version = var.ecs_platform_version - deployment_maximum_percent = var.ecs_deployment_maximum_percent - deployment_minimum_healthy_percent = var.ecs_deployment_minimum_healthy_percent - deployment_circuit_breaker_enabled = var.ecs_deployment_circuit_breaker_enabled - deployment_circuit_breaker_rollback = var.ecs_deployment_circuit_breaker_rollback - container_insights_enabled = var.ecs_container_insights_enabled - - # ALB Target Group ARN for service discovery - target_group_arn = aws_lb_target_group.ecs_tg.arn - - tags = local.common_tags +output "private_subnet_ids" { + description = "Private subnet IDs for ECS tasks" + value = aws_subnet.private[*].id } -# RDS Database -module "rds" { - source = "./modules/rds_database" - - identifier = var.identifier - allocated_storage = var.rds_allocated_storage - instance_class = var.rds_instance_class - db_name = var.postgres_database - username = var.postgres_username - password = var.postgres_password - - private_subnet_ids = aws_subnet.private[*].id - vpc_security_group_ids = [aws_security_group.rds.id] - - enabled_cloudwatch_logs_exports = var.rds_enabled_cloudwatch_logs_exports - performance_insights_enabled = var.rds_performance_insights_enabled - performance_insights_retention_period = var.rds_performance_insights_retention_period - monitoring_interval = var.rds_monitoring_interval - - log_retention_in_days = 30 - - tags = local.common_tags +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 83e33f1d0..0870d1347 100644 --- a/deployment/ecs/modules/ecs/main.tf +++ b/deployment/ecs/modules/ecs/main.tf @@ -1,4 +1,12 @@ -# ECS Cluster +# Data sources +data "aws_region" "current" {} +data "aws_caller_identity" "current" {} + +# Toggle ALB creation inside this module +locals { + do_alb = var.create_alb +} + resource "aws_ecs_cluster" "main" { name = var.cluster_name @@ -10,15 +18,66 @@ resource "aws_ecs_cluster" "main" { tags = var.tags } -# CloudWatch Log Group resource "aws_cloudwatch_log_group" "app_logs" { name = "/ecs/${var.service_name}" retention_in_days = var.log_retention_days + tags = var.tags +} + +resource "aws_iam_role" "ecs_execution_role" { + name = "${var.service_name}-ecs-execution-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { Service = "ecs-tasks.amazonaws.com" } + }] + }) tags = var.tags } -# ECS Task Definition +resource "aws_iam_role" "ecs_task_role" { + name = "${var.service_name}-ecs-task-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { Service = "ecs-tasks.amazonaws.com" } + }] + }) + + tags = var.tags +} + +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" +} + +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 + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Effect = "Allow", + Action = [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + Resource = "*" + }] + }) +} + resource "aws_ecs_task_definition" "app" { family = var.service_name requires_compatibilities = ["FARGATE"] @@ -26,17 +85,17 @@ resource "aws_ecs_task_definition" "app" { cpu = var.cpu memory = var.memory execution_role_arn = aws_iam_role.ecs_execution_role.arn - task_role_arn = aws_iam_role.ecs_task_role.arn + task_role_arn = aws_iam_role.ecs_task_role.arn container_definitions = jsonencode([ { - name = var.service_name - image = var.container_image + name = var.service_name + image = var.container_image essential = true - + portMappings = [ { - containerPort = var.container_port + containerPort = var.container_port # set to 8443 in env protocol = "tcp" } ] @@ -49,19 +108,19 @@ resource "aws_ecs_task_definition" "app" { ] logConfiguration = { - logDriver = "awslogs" + logDriver = "awslogs", options = { - awslogs-group = aws_cloudwatch_log_group.app_logs.name - awslogs-region = data.aws_region.current.name + awslogs-group = aws_cloudwatch_log_group.app_logs.name, + awslogs-region = data.aws_region.current.name, 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"] - interval = 30 - timeout = 5 - retries = 3 + command = ["CMD-SHELL", "curl -f http://localhost:${var.container_port}${var.health_check_path} || exit 1"] + interval = 30 + timeout = 5 + retries = 3 startPeriod = 60 } : null } @@ -70,19 +129,38 @@ resource "aws_ecs_task_definition" "app" { tags = var.tags } -# Security Group for ECS Service + resource "aws_security_group" "ecs_service" { name_prefix = "${var.service_name}-ecs-" vpc_id = var.vpc_id description = "Security group for ${var.service_name} ECS service" - ingress { - from_port = var.container_port - to_port = var.container_port - protocol = "tcp" - cidr_blocks = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] + # Allow from any explicitly provided source SGs (e.g., ALB SG) + dynamic "ingress" { + for_each = var.allowed_source_sg_ids + content { + from_port = var.container_port + to_port = var.container_port + protocol = "tcp" + security_groups = [ingress.value] + description = "Allowed SG -> ECS container port" + } } + # If the ALB is created in this module, also allow from that ALB SG + dynamic "ingress" { + for_each = local.do_alb ? [aws_security_group.alb[0].id] : [] + content { + from_port = var.container_port + to_port = var.container_port + protocol = "tcp" + security_groups = [ingress.value] + description = "Module ALB SG -> ECS container port" + } + } + + + egress { from_port = 0 to_port = 0 @@ -90,43 +168,133 @@ resource "aws_security_group" "ecs_service" { cidr_blocks = ["0.0.0.0/0"] } - tags = merge(var.tags, { - Name = "${var.service_name}-ecs-sg" - }) + tags = merge(var.tags, { Name = "${var.service_name}-ecs-sg" }) + + lifecycle { create_before_destroy = true } +} + + +# ALB SG +resource "aws_security_group" "alb" { + count = local.do_alb ? 1 : 0 + name_prefix = "${var.service_name}-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"] } + + 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 + enable_deletion_protection = var.alb_deletion_protection + 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" + port = var.container_port + protocol = "HTTP" + vpc_id = var.vpc_id + target_type = "ip" + + health_check { + enabled = true + healthy_threshold = 2 + unhealthy_threshold = 2 + timeout = 5 + interval = 30 + path = var.health_check_path + matcher = "200" + port = "traffic-port" + protocol = "HTTP" + } + + tags = var.tags +} - lifecycle { - create_before_destroy = true +# HTTPS listener (443) with TLS termination +resource "aws_lb_listener" "https" { + count = local.do_alb ? 1 : 0 + load_balancer_arn = aws_lb.this[0].arn + port = 443 + protocol = "HTTPS" + ssl_policy = var.ssl_policy + certificate_arn = var.acm_certificate_arn + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.ecs[0].arn } + + tags = var.tags +} + +# HTTP -> HTTPS redirect +resource "aws_lb_listener" "http" { + count = local.do_alb ? 1 : 0 + load_balancer_arn = aws_lb.this[0].arn + port = 80 + protocol = "HTTP" + + default_action { + type = "redirect" + redirect { + port = "443" + protocol = "HTTPS" + 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) } -# ECS Service resource "aws_ecs_service" "app" { - name = var.service_name - cluster = aws_ecs_cluster.main.id - task_definition = aws_ecs_task_definition.app.arn - desired_count = var.desired_count - launch_type = "FARGATE" + name = var.service_name + cluster = aws_ecs_cluster.main.id + task_definition = aws_ecs_task_definition.app.arn + desired_count = var.desired_count + launch_type = "FARGATE" platform_version = var.platform_version network_configuration { - subnets = var.subnet_ids + subnets = var.subnet_ids # private subnets security_groups = [aws_security_group.ecs_service.id] assign_public_ip = var.assign_public_ip } deployment_maximum_percent = var.deployment_maximum_percent deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent - + deployment_circuit_breaker { enable = var.deployment_circuit_breaker_enabled rollback = var.deployment_circuit_breaker_rollback } - # Load Balancer Configuration + # Attach to TG if provided/created dynamic "load_balancer" { - for_each = var.target_group_arn != null ? [1] : [] + for_each = local.effective_tg_arn != null ? [1] : [] content { - target_group_arn = var.target_group_arn + target_group_arn = local.effective_tg_arn container_name = var.service_name container_port = var.container_port } @@ -138,75 +306,3 @@ resource "aws_ecs_service" "app" { aws_iam_role_policy_attachment.ecs_execution_role_policy ] } - -# IAM Role for ECS Execution -resource "aws_iam_role" "ecs_execution_role" { - name = "${var.service_name}-ecs-execution-role" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "ecs-tasks.amazonaws.com" - } - } - ] - }) - - tags = var.tags -} - -# IAM Role for ECS Task -resource "aws_iam_role" "ecs_task_role" { - name = "${var.service_name}-ecs-task-role" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "ecs-tasks.amazonaws.com" - } - } - ] - }) - - tags = var.tags -} - -# Attach AWS managed policy for ECS execution -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" -} - -# Additional policy for ECS execution role to pull from ECR -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 - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - "ecr:GetAuthorizationToken", - "ecr:BatchCheckLayerAvailability", - "ecr:GetDownloadUrlForLayer", - "ecr:BatchGetImage" - ] - Resource = "*" - } - ] - }) -} - -# Data sources -data "aws_region" "current" {} -data "aws_caller_identity" "current" {} \ No newline at end of file diff --git a/deployment/ecs/modules/ecs/outputs.tf b/deployment/ecs/modules/ecs/outputs.tf index 92bbe6871..7dfc0acb7 100644 --- a/deployment/ecs/modules/ecs/outputs.tf +++ b/deployment/ecs/modules/ecs/outputs.tf @@ -8,6 +8,11 @@ output "cluster_arn" { value = aws_ecs_cluster.main.arn } +output "cluster_name" { + description = "Name of the ECS cluster" + value = aws_ecs_cluster.main.name +} + output "service_id" { description = "ID of the ECS service" value = aws_ecs_service.app.id @@ -18,13 +23,18 @@ output "service_arn" { value = "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:service/${aws_ecs_cluster.main.name}/${aws_ecs_service.app.name}" } +output "service_name" { + description = "Name of the ECS service" + value = aws_ecs_service.app.name +} + output "task_definition_arn" { description = "ARN of the task definition" value = aws_ecs_task_definition.app.arn } output "security_group_id" { - description = "ID of the ECS service security group" + description = "ECS tasks security group ID" value = aws_security_group.ecs_service.id } @@ -48,12 +58,34 @@ output "log_group_arn" { value = aws_cloudwatch_log_group.app_logs.arn } -output "cluster_name" { - description = "Name of the ECS cluster" - value = aws_ecs_cluster.main.name + +output "alb_security_group_id" { + description = "ALB security group ID (null if ALB not created in-module)" + value = try(aws_security_group.alb[0].id, null) } -output "service_name" { - description = "Name of the ECS service" - value = aws_ecs_service.app.name -} \ No newline at end of file +output "alb_arn" { + description = "ALB ARN (null if ALB not created in-module)" + value = try(aws_lb.this[0].arn, null) +} + +output "alb_dns_name" { + description = "ALB DNS name (null if ALB not created in-module)" + value = try(aws_lb.this[0].dns_name, null) +} + +output "alb_zone_id" { + description = "Route53 zone ID for ALB alias (null if ALB not created in-module)" + value = try(aws_lb.this[0].zone_id, null) +} + +output "target_group_arn_created" { + description = "ARN of the TG created by the module (null if not created)" + value = try(aws_lb_target_group.ecs[0].arn, null) +} + +# Effective TG used by the service (created or provided via var.target_group_arn) +output "target_group_arn_effective" { + description = "Target Group ARN actually used by the ECS service" + value = local.effective_tg_arn +} diff --git a/deployment/ecs/modules/ecs/variables.tf b/deployment/ecs/modules/ecs/variables.tf index 8ab29a933..7dfa4fc25 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 = 7860 + default = 8443 } variable "desired_count" { @@ -43,7 +43,7 @@ variable "vpc_id" { } variable "subnet_ids" { - description = "Subnet IDs for the ECS service" + description = "PRIVATE subnet IDs for ECS tasks" type = list(string) } @@ -80,7 +80,7 @@ variable "health_check_path" { variable "assign_public_ip" { description = "Assign public IP to ECS tasks" type = bool - default = true + default = false } variable "platform_version" { @@ -119,8 +119,50 @@ variable "container_insights_enabled" { default = true } -variable "target_group_arn" { - description = "ARN of the load balancer target group" +variable "create_alb" { + description = "Create ALB/TG/listeners inside the module" + type = bool + default = false +} + +variable "public_subnet_ids" { + description = "PUBLIC subnet IDs for ALB (required if create_alb = true)" + type = list(string) + default = [] +} + +variable "acm_certificate_arn" { + description = "ACM certificate ARN for HTTPS listener (required if create_alb = true)" + type = string + default = "" +} + +variable "ssl_policy" { + description = "TLS policy for HTTPS listener" + type = string + default = "ELBSecurityPolicy-TLS-1-2-2017-01" +} + +variable "alb_name" { + description = "Name for the ALB (defaults to ${service_name}-alb)" type = string default = null -} \ No newline at end of file +} + +variable "alb_deletion_protection" { + description = "Enable deletion protection on the ALB" + type = bool + default = true +} + +variable "allowed_source_sg_ids" { + description = "Security group IDs allowed to reach the ECS tasks on container_port (e.g., ALB SG). If empty, no SG ingress is created (unless ALB is created in-module)." + type = list(string) + default = [] +} + + +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." +} diff --git a/deployment/ecs/outputs.tf b/deployment/ecs/outputs.tf index 673aeaa48..16378637c 100644 --- a/deployment/ecs/outputs.tf +++ b/deployment/ecs/outputs.tf @@ -66,17 +66,17 @@ output "private_subnet_ids" { output "alb_dns_name" { description = "DNS name of the load balancer" - value = aws_lb.healthmetricsai_production_alb.dns_name + value = aws_lb.mpath_production_alb.dns_name } output "alb_zone_id" { description = "Zone ID of the load balancer" - value = aws_lb.healthmetricsai_production_alb.zone_id + value = aws_lb.mpath_production_alb.zone_id } output "alb_arn" { description = "ARN of the load balancer" - value = aws_lb.healthmetricsai_production_alb.arn + value = aws_lb.mpath_production_alb.arn } output "target_group_arn" { diff --git a/deployment/ecs/terraform.tfvars b/deployment/ecs/terraform.tfvars index 1b53cb1f2..f01c6319c 100644 --- a/deployment/ecs/terraform.tfvars +++ b/deployment/ecs/terraform.tfvars @@ -1,5 +1,5 @@ # AWS Configuration -aws_account_id = "2" # Replace with your AWS account ID +aws_account_id = "211125425735" # Replace with your AWS account ID # Application Configuration environment = "Production" @@ -11,9 +11,6 @@ desired_count = 2 cpu = 1024 memory = 2048 -# Security Configuration -secret_key = "your-secure-secret-key-here" # Generate a secure key - # AWS Region Configuration aws_region = "us-east-1" availability_zones = ["us-east-1a", "us-east-1b"] @@ -31,7 +28,7 @@ certificate_arn = "" tags = { Owner = "DevOps Team" CostCenter = "Engineering" - Application = "HealthMetricsAI" + Application = "mpath" } # ECS Configuration diff --git a/deployment/ecs/variables.tf b/deployment/ecs/variables.tf index a473f4d35..cd88ed65b 100644 --- a/deployment/ecs/variables.tf +++ b/deployment/ecs/variables.tf @@ -29,7 +29,7 @@ variable "availability_zones" { variable "vpc_name" { description = "Name of the VPC" type = string - default = "healthmetricsai-production-vpc" + default = "mpath-production-vpc" validation { condition = length(var.vpc_name) > 0 && length(var.vpc_name) <= 255 @@ -50,7 +50,7 @@ variable "certificate_arn" { variable "internet_gateway_name" { description = "Name of the Internet Gateway" type = string - default = "healthmetricsai-production-igw" + default = "mpath-production-igw" validation { condition = length(var.internet_gateway_name) > 0 && length(var.internet_gateway_name) <= 255 @@ -61,7 +61,7 @@ variable "internet_gateway_name" { variable "nat_gateway_name" { description = "Name of the NAT Gateway" type = string - default = "healthmetricsai-production-nat-gateway" + default = "mpath-production-nat-gateway" validation { condition = length(var.nat_gateway_name) > 0 && length(var.nat_gateway_name) <= 255 diff --git a/deployment/ecs/waf.tf b/deployment/ecs/waf.tf index 43286eac6..17a527377 100644 --- a/deployment/ecs/waf.tf +++ b/deployment/ecs/waf.tf @@ -1,4 +1,4 @@ -resource "aws_wafv2_web_acl" "healthmetricsai_production_web_acl" { +resource "aws_wafv2_web_acl" "mpath_production_web_acl" { name = "${var.environment}-${local.app_name}-web-acl" description = "${var.environment} ${local.app_name} WebACL" scope = "REGIONAL" @@ -157,7 +157,7 @@ resource "aws_wafv2_web_acl" "healthmetricsai_production_web_acl" { tags = local.common_tags } -resource "aws_wafv2_web_acl_association" "healthmetricsai_production_web_acl_association" { - resource_arn = aws_lb.healthmetricsai_production_alb.arn - web_acl_arn = aws_wafv2_web_acl.healthmetricsai_production_web_acl.arn +resource "aws_wafv2_web_acl_association" "mpath_production_web_acl_association" { + resource_arn = aws_lb.mpath_production_alb.arn + web_acl_arn = aws_wafv2_web_acl.mpath_production_web_acl.arn } \ No newline at end of file From 1ba83743abd9effc1de90e464d23d5225dd53d82 Mon Sep 17 00:00:00 2001 From: Joseph Yousefpour Date: Wed, 24 Sep 2025 18:37:22 -0400 Subject: [PATCH 3/5] initial iac --- deployment/ecs/.gitignore | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 deployment/ecs/.gitignore diff --git a/deployment/ecs/.gitignore b/deployment/ecs/.gitignore new file mode 100644 index 000000000..4260bbcff --- /dev/null +++ b/deployment/ecs/.gitignore @@ -0,0 +1,41 @@ +.DS_Store +*.zip +.sql +# Local .terraform directories +**/.terraform/* + +.terraform* +.terraform.lock.hcl +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + + From 7a53935533dfb004e35b0a71de6d5e80463ff67c Mon Sep 17 00:00:00 2001 From: Joseph Yousefpour Date: Tue, 30 Sep 2025 22:42:42 -0400 Subject: [PATCH 4/5] adding ecs iac --- deployment/ecs.zip | Bin 0 -> 17155 bytes deployment/ecs/envs/bo/main.tf | 20 +++- deployment/ecs/envs/bo/terraform.tfvars | 45 +++++--- deployment/ecs/envs/bo/variables.tf | 4 + deployment/ecs/outputs.tf | 11 +- deployment/ecs/terraform.tfvars | 39 +------ deployment/ecs/variables.tf | 9 +- deployment/ecs/waf.tf | 134 ++++-------------------- 8 files changed, 92 insertions(+), 170 deletions(-) create mode 100644 deployment/ecs.zip diff --git a/deployment/ecs.zip b/deployment/ecs.zip new file mode 100644 index 0000000000000000000000000000000000000000..b3e80cc6f154ca8eaa99cb4049e7b15245cc4731 GIT binary patch literal 17155 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 literal 0 HcmV?d00001 diff --git a/deployment/ecs/envs/bo/main.tf b/deployment/ecs/envs/bo/main.tf index 8cd8e89e4..bed591789 100644 --- a/deployment/ecs/envs/bo/main.tf +++ b/deployment/ecs/envs/bo/main.tf @@ -16,10 +16,9 @@ locals { data "terraform_remote_state" "root" { backend = "s3" config = { - bucket = "YOUR_TF_STATE_BUCKET" - key = "mpath/root/terraform.tfstate" # <-- update to your actual root state key - region = "us-east-1" - dynamodb_table = "YOUR_TF_LOCKS_TABLE" + bucket = "mpath-terraform-state" + key = "mpath/root/terraform.tfstate" + region = var.aws_region encrypt = true } } @@ -86,3 +85,16 @@ output "bo_target_group_arn" { 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" { + name = "${local.app_name}-${local.env}-alb" + depends_on = [module.ecs_service] +} + +# Associate the root WAF to this ALB +resource "aws_wafv2_web_acl_association" "mpath_web_acl_assoc" { + resource_arn = data.aws_lb.bo_alb.arn + web_acl_arn = data.terraform_remote_state.root.outputs.waf_web_acl_arn +} + diff --git a/deployment/ecs/envs/bo/terraform.tfvars b/deployment/ecs/envs/bo/terraform.tfvars index 4dac85ec6..cffd85b93 100644 --- a/deployment/ecs/envs/bo/terraform.tfvars +++ b/deployment/ecs/envs/bo/terraform.tfvars @@ -1,26 +1,39 @@ -aws_region = "us-east-1" - -# Container -container_image = "295669632222.dkr.ecr.us-east-1.amazonaws.com/microhealthllc/mpath-bo:latest" -container_port = 8443 -desired_count = 2 -cpu = 512 -memory = 1024 +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 +desired_count = 2 +cpu = 1024 +memory = 2048 + +# Healthcheck health_check_path = "/health" -# TLS for ALB -acm_certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" +# TLS / ALB +acm_certificate_arn = "" # ACM ARN +ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" +alb_deletion_protection = true -# Ops / deployments -log_retention_days = 30 +# 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 +deployment_circuit_breaker_rollback = true +log_retention_days = 30 + + +waf_allowed_countries = ["US"] -# Optional tags +# Tags tags = { - Owner = "Microhealth_Platform" + Owner = "DevOps Team" + CostCenter = "Engineering" + Application = "mpath" } diff --git a/deployment/ecs/envs/bo/variables.tf b/deployment/ecs/envs/bo/variables.tf index 15fcc34c5..b58c58285 100644 --- a/deployment/ecs/envs/bo/variables.tf +++ b/deployment/ecs/envs/bo/variables.tf @@ -24,3 +24,7 @@ variable "tags" { type = map(string) default = {} } + +variable "aws_region" { + default = "us-east-1" +} \ No newline at end of file diff --git a/deployment/ecs/outputs.tf b/deployment/ecs/outputs.tf index 16378637c..5c3cf77fd 100644 --- a/deployment/ecs/outputs.tf +++ b/deployment/ecs/outputs.tf @@ -82,4 +82,13 @@ output "alb_arn" { output "target_group_arn" { description = "ARN of the target group" value = aws_lb_target_group.ecs_tg.arn -} \ No newline at end of file +} + +# root/outputs.tf +output "waf_web_acl_arn" { + value = aws_wafv2_web_acl.mpath_web_acl.arn +} + +output "waf_web_acl_id" { + value = aws_wafv2_web_acl.mpath_web_acl.id +} diff --git a/deployment/ecs/terraform.tfvars b/deployment/ecs/terraform.tfvars index f01c6319c..ac166f2e1 100644 --- a/deployment/ecs/terraform.tfvars +++ b/deployment/ecs/terraform.tfvars @@ -1,28 +1,14 @@ -# AWS Configuration -aws_account_id = "211125425735" # Replace with your AWS account ID - -# Application Configuration -environment = "Production" -container_image_tag = "latest" - - -# ECS Configuration -desired_count = 2 -cpu = 1024 -memory = 2048 - -# AWS Region Configuration aws_region = "us-east-1" availability_zones = ["us-east-1a", "us-east-1b"] -# VPC Configuration +# VPC vpc_name = "mpath-production-vpc" vpc_cidr_block = "192.168.29.0/24" internet_gateway_name = "mpath-production-igw" nat_gateway_name = "mpath-production-nat-gateway" -# SSL Certificate -certificate_arn = "" +# WAF (only if WAF is defined in root/waf.tf) +waf_allowed_countries = ["US"] # Tags tags = { @@ -30,22 +16,3 @@ tags = { CostCenter = "Engineering" Application = "mpath" } - -# ECS Configuration -ecs_log_retention_days = 30 -ecs_health_check_enabled = true -ecs_health_check_path = "/health" -ecs_assign_public_ip = false -ecs_platform_version = "LATEST" -ecs_deployment_maximum_percent = 200 -ecs_deployment_minimum_healthy_percent = 50 -ecs_deployment_circuit_breaker_enabled = true -ecs_deployment_circuit_breaker_rollback = true -ecs_container_insights_enabled = true - -# ALB Configuration -alb_deletion_protection = true -alb_ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" - -# WAF Configuration -waf_allowed_countries = ["US"] \ No newline at end of file diff --git a/deployment/ecs/variables.tf b/deployment/ecs/variables.tf index cd88ed65b..8b5f9dee9 100644 --- a/deployment/ecs/variables.tf +++ b/deployment/ecs/variables.tf @@ -287,4 +287,11 @@ variable "waf_allowed_countries" { description = "List of allowed country codes for WAF geo restriction" type = list(string) default = ["US"] -} \ No newline at end of file +} + +# 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" } +} diff --git a/deployment/ecs/waf.tf b/deployment/ecs/waf.tf index 17a527377..efd036347 100644 --- a/deployment/ecs/waf.tf +++ b/deployment/ecs/waf.tf @@ -1,150 +1,65 @@ -resource "aws_wafv2_web_acl" "mpath_production_web_acl" { +# Minimal, low-blocking WebACL +resource "aws_wafv2_web_acl" "mpath_web_acl" { name = "${var.environment}-${local.app_name}-web-acl" description = "${var.environment} ${local.app_name} WebACL" scope = "REGIONAL" - default_action { - allow {} - } + + default_action { allow {} } + rule { - name = "block-non-us-countries" + name = "block-non-allowed-countries" priority = 0 - statement { not_statement { statement { geo_match_statement { - country_codes = var.waf_allowed_countries + country_codes = var.waf_allowed_countries # e.g., ["US"] } } } } - - action { - block {} - } - + action { block {} } visibility_config { sampled_requests_enabled = true cloudwatch_metrics_enabled = true - metric_name = "block-non-us-countries" + metric_name = "block-non-allowed-countries" } } + # 2) Block known bad IPs (very low false-positive) rule { - name = "AWS-AWSManagedRulesCommonRuleSet" - priority = 3 - - statement { - managed_rule_group_statement { - vendor_name = "AWS" - name = "AWSManagedRulesCommonRuleSet" - - rule_action_override { - name = "SizeRestrictions_BODY" - action_to_use { - allow {} - } - } - rule_action_override { - name = "GenericLFI_BODY" - action_to_use { - allow {} - } - } - rule_action_override { - name = "GenericRFI_BODY" - action_to_use { - count {} - } - } - } - } - - override_action { - none {} - } - - visibility_config { - sampled_requests_enabled = true - cloudwatch_metrics_enabled = true - metric_name = "AWS-AWSManagedRulesCommonRuleSet" - } - } - rule { - name = "AWS-AWSManagedRulesKnownBadInputsRuleSet" - priority = 4 - override_action { - none { - } - } - statement { - managed_rule_group_statement { - name = "AWSManagedRulesKnownBadInputsRuleSet" - vendor_name = "AWS" - } - } - visibility_config { - cloudwatch_metrics_enabled = true - metric_name = "AWS-AWSManagedRulesKnownBadInputsRuleSet" - sampled_requests_enabled = true - } - } - rule { - name = "AWS-AWSManagedRulesLinuxRuleSet" - priority = 5 - override_action { - none { - } - } + name = "AWS-AWSManagedRulesAmazonIpReputationList" + priority = 1 statement { managed_rule_group_statement { - name = "AWSManagedRulesLinuxRuleSet" + name = "AWSManagedRulesAmazonIpReputationList" vendor_name = "AWS" } } + override_action { none {} } # enforce block visibility_config { cloudwatch_metrics_enabled = true - metric_name = "AWS-AWSManagedRulesLinuxRuleSet" + metric_name = "AWS-AWSManagedRulesAmazonIpReputationList" sampled_requests_enabled = true } } + + # 3) Common rules in COUNT mode (observe first, then enforce later) rule { - name = "AWS-AWSManagedRulesUnixRuleSet" - priority = 6 - override_action { - none { - } - } + name = "AWS-AWSManagedRulesCommonRuleSet" + priority = 2 statement { managed_rule_group_statement { - name = "AWSManagedRulesUnixRuleSet" vendor_name = "AWS" + name = "AWSManagedRulesCommonRuleSet" } } + override_action { count {} } # low-risk: no blocking yet visibility_config { - cloudwatch_metrics_enabled = true - metric_name = "AWS-AWSManagedRulesUnixRuleSet" sampled_requests_enabled = true - } - } - rule { - name = "AWS-AWSManagedRulesAmazonIpReputationList" - priority = 7 - override_action { - none { - } - } - statement { - managed_rule_group_statement { - name = "AWSManagedRulesAmazonIpReputationList" - vendor_name = "AWS" - } - } - visibility_config { cloudwatch_metrics_enabled = true - metric_name = "AWS-AWSManagedRulesAmazonIpReputationList" - sampled_requests_enabled = true + metric_name = "AWS-AWSManagedRulesCommonRuleSet" } } @@ -156,8 +71,3 @@ resource "aws_wafv2_web_acl" "mpath_production_web_acl" { tags = local.common_tags } - -resource "aws_wafv2_web_acl_association" "mpath_production_web_acl_association" { - resource_arn = aws_lb.mpath_production_alb.arn - web_acl_arn = aws_wafv2_web_acl.mpath_production_web_acl.arn -} \ No newline at end of file From ef1bc94b71bb33a56ec0eee9b1732f895f502bbb Mon Sep 17 00:00:00 2001 From: Joseph Yousefpour Date: Tue, 30 Sep 2025 22:47:57 -0400 Subject: [PATCH 5/5] update docker build --- .github/workflows/ecr-publish.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ecr-publish.yml b/.github/workflows/ecr-publish.yml index 89ee44aca..5073c76d0 100644 --- a/.github/workflows/ecr-publish.yml +++ b/.github/workflows/ecr-publish.yml @@ -26,7 +26,13 @@ jobs: - name: Build and push Docker image run: | - docker build -t ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/microhealthllc/bo:latest . + # Build image from docker/app/Dockerfile + docker build \ + -f docker/app/Dockerfile \ + -t ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/microhealthllc/bo:latest \ + docker/app + + # Push image docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/microhealthllc/bo:latest - name: Force ECS Service Update