terraform { required_version = ">= 1.5" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } backend "s3" { bucket = "metaflow-access-core-terraform-state" key = "prod/terraform.tfstate" region = "us-east-1" dynamodb_table = "metaflow-access-core-terraform-locks" encrypt = true } } provider "aws" { region = var.region default_tags { tags = local.tags } } data "aws_caller_identity" "current" {} data "aws_partition" "current" {} locals { name = "access-core-prod" project = "metaflow-access-core" tags = { Project = local.project Environment = "prod" ManagedBy = "terraform" } # Task role name is fixed by the ecs module ("${name}-fargate-task"); building # the ARN here (instead of reading it from the module) avoids a kms<->ecs cycle. task_role_arn = "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:role/${local.name}-fargate-task" # ELB wildcard keeps the ALB reachable before/while a custom domain CNAME exists; # set public_domain once Route53 is wired. allowed_hosts = join(",", compact([var.public_domain, "*.elb.amazonaws.com"])) } # ------------------------------------------------------------------- secrets resource "random_password" "db" { length = 32 special = false } resource "aws_secretsmanager_secret" "db" { name = "${local.name}-rds-password" description = "RDS password for access-core-prod (rotation required after first apply)" } resource "aws_secretsmanager_secret_version" "db" { secret_id = aws_secretsmanager_secret.db.id secret_string = jsonencode({ password = random_password.db.result }) } resource "random_string" "django_secret_key" { length = 50 special = false } resource "aws_secretsmanager_secret" "app" { name = "${local.name}-app-secrets" description = "Application secrets for access-core-prod (rotation required after first apply)" } resource "aws_secretsmanager_secret_version" "app" { secret_id = aws_secretsmanager_secret.app.id secret_string = jsonencode({ django-secret-key = random_string.django_secret_key.result }) } # ------------------------------------------------------------------------ KMS module "kms" { source = "../../modules/kms" name = local.name tags = local.tags # admin defaults to account root; pin via -var for tighter control app_principal_arns = [local.task_role_arn] # task role must be in the key policy (restrictive policy) } # ---------------------------------------------------------------- vpc + net module "vpc" { source = "../../modules/vpc" name = local.name cidr_block = var.vpc_cidr az_count = 2 # prod: multi-AZ (AWS_Cost_Estimate.md) enable_egress_nat = true # Fargate tasks must reach the CMS ACCESS API (non-AWS egress) tags = local.tags } module "endpoints" { source = "../../modules/endpoints" name = local.name region = var.region vpc_id = module.vpc.vpc_id vpc_cidr = module.vpc.vpc_cidr private_subnet_ids = module.vpc.private_subnet_ids private_route_table_id = module.vpc.private_route_table_id services = ["s3", "secretsmanager", "kms", "sqs"] tags = local.tags } # ------------------------------------------------------------------- storage module "storage" { source = "../../modules/storage" bucket_name = "${local.project}-prod-artifacts" sse_kms = true kms_key_arn = module.kms.phi_field_key_arn versioned = true tags = local.tags } # ---------------------------------------------------------------------- queue module "queue" { source = "../../modules/queue" name = local.name kms_key_arn = module.kms.phi_field_key_arn tags = local.tags } # --------------------------------------------------------------------- database module "database" { source = "../../modules/database" name = local.name vpc_id = module.vpc.vpc_id private_subnet_ids = module.vpc.private_subnet_ids vpc_cidr = module.vpc.vpc_cidr password = random_password.db.result instance_class = "db.t3.medium" # prod sizing per AWS_Cost_Estimate.md multi_az = true storage_encrypted = true kms_key_id = module.kms.phi_field_key_arn backup_retention_days = 14 deletion_protection = true tags = local.tags } # ------------------------------------------------------------------- alerting module "alerting" { source = "../../modules/alerting" name = local.name alert_emails = var.alert_emails tags = local.tags } # ------------------------------------------------------------------------ ecs module "ecs" { source = "../../modules/ecs" name = local.name environment = "prod" region = var.region cluster_name = local.name vpc_id = module.vpc.vpc_id public_subnet_ids = module.vpc.public_subnet_ids private_subnet_ids = module.vpc.private_subnet_ids image_tag = var.image_tag alb_certificate_arn = var.alb_certificate_arn django_settings_module = "access_core.settings.base" postgres_host = module.database.address db_secret_arn = aws_secretsmanager_secret.db.arn app_secrets_arn = aws_secretsmanager_secret.app.arn s3_bucket_arn = module.storage.bucket_arn queue_arn = module.queue.webhook_queue_arn kms_key_arn = module.kms.phi_field_key_arn pseudonymization_key_arn = module.kms.phi_pseudonymization_key_arn queue_kms_key_arn = module.kms.phi_field_key_arn enable_kms_queue_access = true desired_count = var.autoscale_min enable_autoscaling = true autoscale_min = var.autoscale_min autoscale_max = var.autoscale_max enable_alarms = true alarm_action_arns = [module.alerting.topic_arn] extra_environment = { PHI_KMS_KEY_ID = module.kms.phi_field_key_arn # KMS ARN is a valid KeyId for boto3 ALLOWED_HOSTS = local.allowed_hosts # base settings default (localhost) would 400 at the ALB AWS_S3_BUCKET = module.storage.bucket_name } tags = local.tags }