From 30c7133b9ae653e20c7dc5ccbe6263d18e95ca3e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 14 Jul 2026 09:38:07 +0000 Subject: [PATCH] =?UTF-8?q?Add=20ara=5Fexport=20Lambda=20+=20SQS=20terrafo?= =?UTF-8?q?rm=20module=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-contained root module mirroring bulk_document_download (4GB memory per ADR-0065, exports-bucket-only IAM). Cross-module wiring (shared ECR/state, fast-api remote_state + ARA_EXPORT_SQS_URL env, CI image build) still needs a terraform plan review — unverifiable in this environment. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../terraform/lambda/ara_export/main.tf | 84 +++++++++++++++++++ .../terraform/lambda/ara_export/outputs.tf | 9 ++ .../terraform/lambda/ara_export/provider.tf | 20 +++++ .../terraform/lambda/ara_export/variables.tf | 61 ++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 deployment/terraform/lambda/ara_export/main.tf create mode 100644 deployment/terraform/lambda/ara_export/outputs.tf create mode 100644 deployment/terraform/lambda/ara_export/provider.tf create mode 100644 deployment/terraform/lambda/ara_export/variables.tf diff --git a/deployment/terraform/lambda/ara_export/main.tf b/deployment/terraform/lambda/ara_export/main.tf new file mode 100644 index 000000000..04978e69e --- /dev/null +++ b/deployment/terraform/lambda/ara_export/main.tf @@ -0,0 +1,84 @@ +############################################ +# ara_export Lambda (ADR-0065) — builds one branded sheet-per-scenario .xlsx +# from persisted modelling data, uploads it to the exports bucket, and emails a +# presigned link. Reads only the DB (no source-bucket reads, unlike +# bulk_document_download), so the role needs only exports-bucket write/presign. +############################################ +data "aws_secretsmanager_secret_version" "db_credentials" { + secret_id = "${var.stage}/assessment_model/db_credentials" +} + +data "aws_secretsmanager_secret_version" "ses_smtp" { + secret_id = "${var.stage}/ses/smtp_credentials" +} + +locals { + db_credentials = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string) + ses_smtp = jsondecode(data.aws_secretsmanager_secret_version.ses_smtp.secret_string) + + document_exports_bucket = "retrofit-document-exports-${var.stage}" +} + +############################################ +# Lambda + SQS queue + trigger +############################################ +module "lambda" { + source = "../../modules/lambda_with_sqs" + + name = var.lambda_name + stage = var.stage + + image_uri = local.image_uri + + reserved_concurrent_executions = var.reserved_concurrent_executions + + batch_size = var.batch_size + maximum_concurrency = var.maximum_concurrency + + timeout = 900 + # Normal-mode openpyxl holds the workbook in memory (ADR-0065); 4 GB covers the + # 100k-property ceiling. Raise here (then switch to write_only) if it OOMs. + memory_size = 4096 + + environment = { + STAGE = var.stage + LOG_LEVEL = "info" + + POSTGRES_USERNAME = local.db_credentials.db_assessment_model_username + POSTGRES_PASSWORD = local.db_credentials.db_assessment_model_password + POSTGRES_HOST = var.db_host + POSTGRES_DATABASE = var.db_name + POSTGRES_PORT = var.db_port + + DOCUMENT_EXPORTS_BUCKET = local.document_exports_bucket + + # SES SMTP — IAM-user credentials sourced from Secrets Manager (modules/ses). + SES_SMTP_HOST = "email-smtp.eu-west-2.amazonaws.com" + SES_SMTP_PORT = "587" + SES_SMTP_USERNAME = local.ses_smtp.username + SES_SMTP_PASSWORD = local.ses_smtp.password + SES_SMTP_FROM_ADDRESS = var.ses_from_address + } +} + +############################################ +# IAM: write + presign-read the workbook on the dedicated exports bucket. +# GetObject is required so the Lambda-signed presigned GET URL resolves. +############################################ +module "s3_exports" { + source = "../../modules/s3_iam_policy" + + policy_name = "AraExportS3Exports-${var.stage}" + policy_description = "Allow ara_export Lambda to write and presign-read Scenario Exports on the exports bucket" + bucket_arns = ["arn:aws:s3:::${local.document_exports_bucket}"] + actions = ["s3:PutObject", "s3:GetObject"] + resource_paths = ["/*"] +} + +resource "aws_iam_role_policy_attachment" "s3_exports" { + role = module.lambda.role_name + policy_arn = module.s3_exports.policy_arn +} + +# NOTE: no ses:* on the role — the handler sends over SMTP with the SES IAM-user +# credentials injected above. diff --git a/deployment/terraform/lambda/ara_export/outputs.tf b/deployment/terraform/lambda/ara_export/outputs.tf new file mode 100644 index 000000000..08e258638 --- /dev/null +++ b/deployment/terraform/lambda/ara_export/outputs.tf @@ -0,0 +1,9 @@ +output "ara_export_queue_url" { + value = module.lambda.queue_url + description = "URL of the ara-export SQS queue (FastAPI enqueues jobs here)" +} + +output "ara_export_queue_arn" { + value = module.lambda.queue_arn + description = "ARN of the ara-export SQS queue" +} diff --git a/deployment/terraform/lambda/ara_export/provider.tf b/deployment/terraform/lambda/ara_export/provider.tf new file mode 100644 index 000000000..ec65a064b --- /dev/null +++ b/deployment/terraform/lambda/ara_export/provider.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } + + backend "s3" { + bucket = "ara-export-terraform-state" + key = "terraform.tfstate" + region = "eu-west-2" + } + + required_version = ">= 1.2.0" +} + +provider "aws" { + region = "eu-west-2" +} diff --git a/deployment/terraform/lambda/ara_export/variables.tf b/deployment/terraform/lambda/ara_export/variables.tf new file mode 100644 index 000000000..e8425d492 --- /dev/null +++ b/deployment/terraform/lambda/ara_export/variables.tf @@ -0,0 +1,61 @@ +variable "lambda_name" { + type = string + description = "Logical name of the lambda" +} + +variable "stage" { + description = "Deployment stage (e.g. dev, prod)" + type = string +} + +variable "ecr_repo_url" { + type = string + description = "ECR repository URL (no tag, no digest)" +} + +variable "image_digest" { + type = string + description = "Image digest (sha256:...)" +} + +variable "reserved_concurrent_executions" { + type = number + default = -1 + description = "Reserved concurrency for the Lambda. -1 = unreserved." +} + +variable "maximum_concurrency" { + type = number + default = 5 + description = "Maximum concurrent Lambda invocations from the SQS trigger." +} + +variable "batch_size" { + type = number + default = 1 +} + +variable "db_host" { + type = string + sensitive = true +} + +variable "db_name" { + type = string + sensitive = true +} + +variable "db_port" { + type = string + sensitive = true +} + +variable "ses_from_address" { + type = string + description = "Verified SES sender for Scenario Export notifications (ADR-0059)." + default = "noreply@domna.homes" +} + +locals { + image_uri = "${var.ecr_repo_url}@${var.image_digest}" +}