terraform { required_providers { aws = { source = "hashicorp/aws" version = ">= 5.0" } } backend "s3" { bucket = "assessment-model-terraform-state" region = "eu-west-2" key = "terraform.tfstate" } required_version = ">= 1.2.0" } provider "aws" { region = var.region } # Additional provider for resources that need to be in us-east-1, specifically the SSL certificate provider "aws" { alias = "aws_use1" region = "us-east-1" } # Assuming the secret is already created and the name is "/assessment_model/db_credentials" data "aws_secretsmanager_secret" "db_credentials" { name = "${var.stage}/assessment_model/db_credentials" } data "aws_secretsmanager_secret_version" "db_credentials" { secret_id = data.aws_secretsmanager_secret.db_credentials.id } # Default VPC data "aws_vpc" "default" { default = true } # For MVP, we allow all inbound traffic to the DB - this will need to be changed later; we'll likely # need to re-deploy the frontend to AWS so that it's within the same VPC as the DB resource "aws_security_group" "allow_db" { name = "allow_tls" description = "Allow TLS inbound traffic" vpc_id = data.aws_vpc.default.id ingress { # TLS (change to whatever ports you need) from_port = 5432 to_port = 5432 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"] } } resource "aws_db_instance" "default" { allocated_storage = var.allocated_storage engine = "postgres" engine_version = "14.17" instance_class = var.instance_class db_name = var.database_name username = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)["db_assessment_model_username"] password = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)["db_assessment_model_password"] parameter_group_name = "default.postgres14" skip_final_snapshot = true vpc_security_group_ids = [aws_security_group.allow_db.id] lifecycle { prevent_destroy = true } # For the moment, we make the database publically accessible so that we can connect to it from the frontend. # We will look to change this in the future but as we are pre-MVP at the time of setting this, we don't # have major security demand and don't want to set this up now publicly_accessible = true # Specify the CA certificate with the default RDS CA certificate ca_cert_identifier = "rds-ca-rsa2048-g1" # Temporary to enfore immediate change apply_immediately = true # Set up storage type to gp3 for better performance storage_type = "gp3" # Automated backups configuration backup_retention_period = 14 backup_window = "03:00-04:00" maintenance_window = "Sun:02:00-Sun:02:30" copy_tags_to_snapshot = true deletion_protection = true } # Set up the bucket that recieve the csv uploads of epc to be retrofit module "s3_presignable_bucket" { source = "../modules/s3_presignable_bucket" bucketname = "retrofit-plan-inputs-${var.stage}" environment = var.stage allowed_origins = var.allowed_origins } output "retrofit_plan_trigger_bucket_name" { value = module.s3_presignable_bucket.bucket_name description = "Name of the retrofit plan trigger bucket" } module "s3_due_considerations_bucket" { source = "../modules/s3_presignable_bucket" bucketname = "retrofit-due-considerations-${var.stage}" environment = var.stage allowed_origins = var.allowed_origins } module "s3_eco_spreadseet_bucket" { source = "../modules/s3_presignable_bucket" bucketname = "retrofit-eco-spreadsheet-${var.stage}" environment = var.stage allowed_origins = var.allowed_origins } module "s3" { source = "../modules/s3" bucketname = "retrofit-datalake-${var.stage}" allowed_origins = var.allowed_origins } output "retrofit_datalake_bucket_id" { value = module.s3.bucket_id } output "retrofit_datalake_bucket_arn" { value = module.s3.bucket_arn } output "retrofit_datalake_bucket_domain_name" { value = module.s3.bucket_domain_name } output "retrofit_datalake_bucket_name" { value = module.s3.bucket_name } module "model_directory" { source = "../modules/s3" bucketname = "retrofit-model-directory-${var.stage}" allowed_origins = var.allowed_origins } module "retrofit_sap_predictions" { source = "../modules/s3" bucketname = "retrofit-sap-predictions-${var.stage}" allowed_origins = var.allowed_origins } output "retrofit_sap_predictions_bucket_name" { value = module.retrofit_sap_predictions.bucket_name description = "Name of the retrofit SAP predictions bucket" } module "retrofit_sap_data" { source = "../modules/s3" bucketname = "retrofit-data-${var.stage}" allowed_origins = var.allowed_origins } output "retrofit_sap_data_bucket_name" { value = module.retrofit_sap_data.bucket_name description = "Name of the retrofit SAP data bucket" } module "retrofit_carbon_predictions" { source = "../modules/s3" bucketname = "retrofit-carbon-predictions-${var.stage}" allowed_origins = var.allowed_origins } output "retrofit_carbon_predictions_bucket_name" { value = module.retrofit_carbon_predictions.bucket_name description = "Name of the retrofit carbon predictions bucket" } module "retrofit_heat_predictions" { source = "../modules/s3" bucketname = "retrofit-heat-predictions-${var.stage}" allowed_origins = var.allowed_origins } output "retrofit_heat_predictions_bucket_name" { value = module.retrofit_heat_predictions.bucket_name description = "Name of the retrofit heat predictions bucket" } module "retrofit_lighting_cost_predictions" { source = "../modules/s3" bucketname = "retrofit-lighting-cost-predictions-${var.stage}" allowed_origins = var.allowed_origins } module "retrofit_heating_cost_predictions" { source = "../modules/s3" bucketname = "retrofit-heating-cost-predictions-${var.stage}" allowed_origins = var.allowed_origins } module "retrofit_hot_water_cost_predictions" { source = "../modules/s3" bucketname = "retrofit-hot-water-cost-predictions-${var.stage}" allowed_origins = var.allowed_origins } module "retrofit_heating_kwh_predictions" { source = "../modules/s3" bucketname = "retrofit-heating-kwh-predictions-${var.stage}" allowed_origins = var.allowed_origins } output "retrofit_heating_kwh_predictions_bucket_name" { value = module.retrofit_heating_kwh_predictions.bucket_name description = "Name of the retrofit heating kWh predictions bucket" } module "retrofit_hotwater_kwh_predictions" { source = "../modules/s3" bucketname = "retrofit-hotwater-kwh-predictions-${var.stage}" allowed_origins = var.allowed_origins } output "retrofit_hotwater_kwh_predictions_bucket_name" { value = module.retrofit_hotwater_kwh_predictions.bucket_name description = "Name of the retrofit hotwater kWh predictions bucket" } module "retrofit_sap_baseline_predictions" { source = "../modules/s3" bucketname = "retrofit-sap-baseline-predictions-${var.stage}" allowed_origins = var.allowed_origins } output "retrofit_sap_baseline_predictions_bucket_name" { value = module.retrofit_sap_baseline_predictions.bucket_name description = "Name of the retrofit SAP baseline predictions bucket" } module "retrofit_carbon_baseline_predictions" { source = "../modules/s3" bucketname = "retrofit-carbon-baseline-predictions-${var.stage}" allowed_origins = var.allowed_origins } output "retrofit_carbon_baseline_predictions_bucket_name" { value = module.retrofit_carbon_baseline_predictions.bucket_name description = "Name of the retrofit carbon baseline predictions bucket" } module "retrofit_heat_baseline_predictions" { source = "../modules/s3" bucketname = "retrofit-heat-baseline-predictions-${var.stage}" allowed_origins = var.allowed_origins } output "retrofit_heat_baseline_predictions_bucket_name" { value = module.retrofit_heat_baseline_predictions.bucket_name description = "Name of the retrofit heat baseline predictions bucket" } // We make this bucket presignable, because we want to generate download links for the frontend module "retrofit_energy_assessments" { source = "../modules/s3_presignable_bucket" bucketname = "retrofit-energy-assessments-${var.stage}" allowed_origins = var.allowed_origins environment = var.stage enable_versioning = true } output "retrofit_energy_assessments_bucket_name" { value = module.retrofit_energy_assessments.bucket_name description = "Name of the retrofit energy assessments bucket" } module "energy_assessments_s3_write" { source = "../modules/s3_iam_policy" policy_name = "EnergyAssessmentsWriteS3" policy_description = "Allow lambdas to write to retrofit energy assessments bucket" bucket_arns = ["arn:aws:s3:::retrofit-energy-assessments-${var.stage}"] actions = ["s3:PutObject", "s3:AbortMultipartUpload"] resource_paths = ["/*"] } output "energy_assessments_s3_write_arn" { value = module.energy_assessments_s3_write.policy_arn } # Set up the route53 record for the API module "route53" { source = "../modules/route53" domain_name = var.domain_name api_url_prefix = var.api_url_prefix providers = { aws.aws_use1 = aws.aws_use1 } } # Create an ECR repository for storage of the lambda's docker images module "ecr" { ecr_name = "fastapi-repository-${var.stage}" source = "../modules/ecr" } module "lambda_sap_prediction_ecr" { ecr_name = "lambda-sap-prediction-${var.stage}" source = "../modules/ecr" } module "due_considerations_ecr" { ecr_name = "due-considerations-${var.stage}" source = "../modules/ecr" } module "eco_spreadsheet_ecr" { ecr_name = "eco-spreadsheet-${var.stage}" source = "../modules/ecr" } module "lambda_carbon_prediction_ecr" { ecr_name = "lambda-carbon-prediction-${var.stage}" source = "../modules/ecr" } module "lambda_heat_prediction_ecr" { ecr_name = "lambda-heat-prediction-${var.stage}" source = "../modules/ecr" } # ECR repos for lighting cost, heating cost and hot water cost models module "lambda_lighting_cost_prediction_ecr" { ecr_name = "lighting-cost-prediction-${var.stage}" source = "../modules/ecr" } module "lambda_heating_cost_prediction_ecr" { ecr_name = "heating-cost-prediction-${var.stage}" source = "../modules/ecr" } module "lambda_hot_water_cost_prediction_ecr" { ecr_name = "hot-water-cost-prediction-${var.stage}" source = "../modules/ecr" } # For heating and hot water kwh models module "lambda_heating_kwh_prediction_ecr" { ecr_name = "heating-kwh-prediction-${var.stage}" source = "../modules/ecr" } module "lambda_hotwater_kwh_prediction_ecr" { ecr_name = "hotwater-kwh-prediction-${var.stage}" source = "../modules/ecr" } # Baselining models module "sap_baseline_ecr" { ecr_name = "sap-baseline-prediction-${var.stage}" source = "../modules/ecr" } module "heat_baseline_ecr" { ecr_name = "heat-baseline-prediction-${var.stage}" source = "../modules/ecr" } module "carbon_baseline_ecr" { ecr_name = "carbon-baseline-prediction-${var.stage}" source = "../modules/ecr" } ################################################ # SES - Email sending ################################################ module "ses" { source = "../modules/ses" domain_name = "domna.homes" stage = var.stage } output "ses_dns_records" { value = module.ses.dns_records } ################################################ # Address2UPRN – Lambda ECR ################################################ module "address2uprn_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "address2uprn-terraform-state" } module "address2uprn_registry" { source = "../modules/container_registry" name = "address2uprn" stage = var.stage } # S3 policy for postcode splitter to read from retrofit data bucket module "address2uprn_s3_read_and_write" { source = "../modules/s3_iam_policy" policy_name = "Address2UPRNReadandWriteS3" policy_description = "Allow address2uprn Lambda to read and write from retrofit-data bucket" bucket_arns = ["arn:aws:s3:::retrofit-data-${var.stage}"] actions = ["s3:GetObject", "s3:ListBucket", "s3:PutObject"] resource_paths = ["/*"] } output "address_2_uprn_s3_read_and_write_arn" { value = module.address2uprn_s3_read_and_write.policy_arn } ################################################ # Condition ETL – Lambda ECR ################################################ module "condition_etl_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "condition-etl-terraform-state" } module "condition_etl_registry" { source = "../modules/container_registry" name = "condition-etl" stage = var.stage } # Condition Data S3 Bucket to store initial data module "condition_data_bucket" { source = "../modules/s3" bucketname = "condition-data-${var.stage}" allowed_origins = var.allowed_origins } module "condition_etl_s3_read" { source = "../modules/s3_iam_policy" policy_name = "ConditionETLReadS3" policy_description = "Allow Lambda to read objects from condition-data-${var.stage}" bucket_arns = ["arn:aws:s3:::condition-data-${var.stage}"] actions = ["s3:GetObject"] resource_paths = ["/*"] } output "condition_etl_s3_read_arn" { value = module.condition_etl_s3_read.policy_arn } ################################################ # Postcode Splitter – Lambda ECR ################################################ module "postcode_splitter_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "postcode-splitter-terraform-state" } module "postcode_splitter_registry" { source = "../modules/container_registry" name = "postcode_splitter" stage = var.stage } # S3 policy for postcode splitter to read from retrofit data bucket module "postcode_splitter_s3_read" { source = "../modules/s3_iam_policy" policy_name = "PostcodeSplitterReadS3" policy_description = "Allow postcode splitter Lambda to read from retrofit-data bucket" bucket_arns = ["arn:aws:s3:::retrofit-data-${var.stage}"] actions = ["s3:GetObject", "s3:ListBucket", "s3:PutObject"] resource_paths = ["/*"] } output "postcode_splitter_s3_read_arn" { value = module.postcode_splitter_s3_read.policy_arn } ################################################ # Landlord Description Overrides – Lambda ################################################ module "landlord_description_overrides_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "landlord-description-overrides-terraform-state" } module "landlord_description_overrides_registry" { source = "../modules/container_registry" name = "landlord_description_overrides" stage = var.stage } # S3 policy for the landlord classifier to read the original upload CSV. module "landlord_overrides_s3_read" { source = "../modules/s3_iam_policy" policy_name = "LandlordOverridesReadS3" # NOTE: aws_iam_policy.description is ForceNew — changing it destroys+recreates the # policy, which deadlocks because the policy is attached to the lambda role in the # separate landlordDescriptionOverrides stack (DeleteConflict). Keep this string # byte-for-byte identical to what's in state so the bucket change applies in-place. policy_description = "Allow landlord description overrides Lambda to read from retrofit-data bucket" bucket_arns = [ "arn:aws:s3:::retrofit-plan-inputs-${var.stage}", "arn:aws:s3:::retrofit-data-${var.stage}", ] actions = ["s3:GetObject", "s3:ListBucket"] resource_paths = ["/*"] } output "landlord_overrides_s3_read_arn" { value = module.landlord_overrides_s3_read.policy_arn } ################################################ # Bulk Address2UPRN Combiner – Lambda ECR ################################################ module "bulk_address2uprn_combiner_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "bulk-address2uprn-combiner-terraform-state" } module "bulk_address2uprn_combiner_registry" { source = "../modules/container_registry" name = "bulk_address2uprn_combiner" stage = var.stage } module "bulk_address2uprn_combiner_s3" { source = "../modules/s3_iam_policy" policy_name = "BulkAddress2UprnCombinerS3" policy_description = "Allow bulk_address2uprn_combiner Lambda to read ara_raw_outputs and write bulk_final_outputs" bucket_arns = ["arn:aws:s3:::retrofit-data-${var.stage}"] actions = ["s3:GetObject", "s3:ListBucket", "s3:PutObject"] resource_paths = ["/ara_raw_outputs/*", "/bulk_final_outputs/*"] } output "bulk_address2uprn_combiner_s3_arn" { value = module.bulk_address2uprn_combiner_s3.policy_arn } ################################################ # Bulk Upload Finaliser – Lambda ECR (ADR-0013) ################################################ module "bulk_upload_finaliser_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "bulk-upload-finaliser-terraform-state" } module "bulk_upload_finaliser_registry" { source = "../modules/container_registry" name = "bulk_upload_finaliser" stage = var.stage } # The finaliser reads the combiner output (bulk_final_outputs) to insert property # rows, and — for v2 (ADR-0006) — the classifier CSV (bulk_onboarding_inputs) to # populate property_overrides. It writes to Postgres, not S3. module "bulk_upload_finaliser_s3_read" { source = "../modules/s3_iam_policy" policy_name = "BulkUploadFinaliserReadS3" policy_description = "Allow bulk_upload_finaliser Lambda to read combiner output from retrofit-data bucket" bucket_arns = ["arn:aws:s3:::retrofit-data-${var.stage}"] actions = ["s3:GetObject", "s3:ListBucket"] resource_paths = ["/bulk_final_outputs/*", "/bulk_onboarding_inputs/*"] } output "bulk_upload_finaliser_s3_read_arn" { value = module.bulk_upload_finaliser_s3_read.policy_arn } ################################################ # Categorisation – Lambda ECR ################################################ module "categorisation_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "categorisation-terraform-state" } module "categorisation_registry" { source = "../modules/container_registry" name = "categorisation" stage = var.stage } ################################################ # OrdnanceSurveyAPI – Lambda ################################################ module "ordnance_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "ordnance-terraform-state" } module "ordnance_registry" { source = "../modules/container_registry" name = "ordnance" stage = var.stage } # S3 policy for postcode splitter to read from retrofit data bucket module "ordnance_s3_read_and_write" { source = "../modules/s3_iam_policy" policy_name = "OrdnanceSurveyReadandWriteS3" policy_description = "Allow ordnance Lambda to read and write from retrofit-data bucket" bucket_arns = ["arn:aws:s3:::retrofit-data-${var.stage}"] actions = ["s3:GetObject", "s3:ListBucket", "s3:PutObject"] resource_paths = ["/*"] } output "ordnance_s3_read_and_write_arn" { value = module.ordnance_s3_read_and_write.policy_arn } ################################################ # Pas Hub to Ara – Lambda ################################################ module "pashub_to_ara_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "pashub-to-ara-terraform-state" } module "pashub_to_ara_registry" { source = "../modules/container_registry" name = "pashub_to_ara" stage = var.stage } #### TEMP - need to unattach from entities before this can be delete #### module "pashub_to_ara_s3_write" { source = "../modules/s3_iam_policy" policy_name = "PashubToAraWriteS3" policy_description = "Allow PasHub to ARA Lambda to write to retrofit energy assessments bucket" bucket_arns = ["arn:aws:s3:::retrofit-energy-assessments-${var.stage}"] actions = ["s3:PutObject", "s3:AbortMultipartUpload"] resource_paths = ["/*"] } output "pashub_to_ara_s3_write_arn" { value = module.pashub_to_ara_s3_write.policy_arn } ################################################ # ECMK to Ara – Lambda ################################################ module "ecmk_to_ara_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "ecmk-to-ara-terraform-state" } module "ecmk_to_ara_registry" { source = "../modules/container_registry" name = "ecmk_to_ara" stage = var.stage } ################################################ # Engine – Lambda ECR ################################################ module "engine_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "ara-engine-terraform-state" } module "engine_registry" { source = "../modules/container_registry" name = "engine" stage = var.stage } # S3 policy for Engine to read and write from various S3 buckets module "engine_s3_read_and_write" { source = "../modules/s3_iam_policy" policy_name = "EngineReadandWriteS3" policy_description = "Allow Engine Lambda to read from and write to various S3 buckets" bucket_arns = [ "arn:aws:s3:::${module.s3_presignable_bucket.bucket_name}", "arn:aws:s3:::${module.retrofit_sap_data.bucket_name}", "arn:aws:s3:::${module.retrofit_sap_predictions.bucket_name}", "arn:aws:s3:::${module.retrofit_carbon_predictions.bucket_name}", "arn:aws:s3:::${module.retrofit_heat_predictions.bucket_name}", "arn:aws:s3:::${module.retrofit_heating_kwh_predictions.bucket_name}", "arn:aws:s3:::${module.retrofit_hotwater_kwh_predictions.bucket_name}", "arn:aws:s3:::${module.retrofit_energy_assessments.bucket_name}", "arn:aws:s3:::${module.retrofit_sap_baseline_predictions.bucket_name}", "arn:aws:s3:::${module.retrofit_carbon_baseline_predictions.bucket_name}", "arn:aws:s3:::${module.retrofit_heat_baseline_predictions.bucket_name}" ] actions = ["s3:*"] resource_paths = ["/*"] } output "engine_s3_read_and_write_arn" { value = module.engine_s3_read_and_write.policy_arn } ################################################ # FastAPI – Lambda ################################################ module "ara_fast_api_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "ara-fast-api-terraform-state" } output "ara_fast_api_state_bucket" { value = module.ara_fast_api_state_bucket.bucket_name } # S3 policy for FastAPI app to read and write from various S3 buckets module "fast_api_s3_read_and_write" { source = "../modules/s3_iam_policy" policy_name = "FastAPIReadandWriteS3" policy_description = "Allow FastAPI Lambda to read from and write to various S3 buckets" bucket_arns = [ "arn:aws:s3:::${module.s3_presignable_bucket.bucket_name}", "arn:aws:s3:::${module.retrofit_sap_data.bucket_name}", "arn:aws:s3:::${module.retrofit_sap_predictions.bucket_name}", "arn:aws:s3:::${module.retrofit_carbon_predictions.bucket_name}", "arn:aws:s3:::${module.retrofit_heat_predictions.bucket_name}", "arn:aws:s3:::${module.retrofit_heating_kwh_predictions.bucket_name}", "arn:aws:s3:::${module.retrofit_hotwater_kwh_predictions.bucket_name}", "arn:aws:s3:::${module.retrofit_energy_assessments.bucket_name}" ] actions = ["s3:GetObject", "s3:ListBucket"] resource_paths = ["/*"] } output "fast_api_s3_read_and_write_arn" { value = module.fast_api_s3_read_and_write.policy_arn } ################################################ # CDN Certificate ################################################ module "cdn_certificate_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "cdn-certificate-terraform-state" } output "cdn_certificate_state_bucket" { value = module.cdn_certificate_state_bucket.bucket_name } ################################################ # CDN ################################################ module "cdn_state_bucket" { source = "../modules/tf_state_bucket" bucket_name = "ara-cdn-terraform-state" } output "cdn_state_bucket" { value = module.cdn_state_bucket.bucket_name } ################################################ # Hubspot ETL Lambda ################################################ module "hubspot_etl_bucket" { source = "../modules/tf_state_bucket" bucket_name = "hubspot-etl-bucket-terraform-state" } module "hubspot_etl_registry" { source = "../modules/container_registry" name = "hubspot-etl" stage = var.stage } # S3 policy for postcode splitter to read from retrofit data bucket module "hubspot_etl_s3_read_and_write" { source = "../modules/s3_iam_policy" policy_name = "HubspotETLReadandWriteS3" policy_description = "Allow hubspot_etl_lambda Lambda to read and write from retrofit-data bucket" bucket_arns = ["arn:aws:s3:::retrofit-data-${var.stage}"] actions = ["s3:GetObject", "s3:ListBucket", "s3:PutObject"] resource_paths = ["/*"] } output "hubspot_etl_s3_read_and_write_arn" { value = module.hubspot_etl_s3_read_and_write.policy_arn } ################################################ # MagicPlan Client – Lambda ################################################ module "magic_plan_client_bucket" { source = "../modules/tf_state_bucket" bucket_name = "magic-plan-client-terraform-state" } module "magic_plan_client_registry" { source = "../modules/container_registry" name = "magic-plan" stage = var.stage }