# shared-memory — Terraform module (AWS Fargate) Deploys [shared-memory](../README.md) to AWS Fargate ECS behind an ALB. Brings up the `app` (Next.js web + MCP endpoint), the `embedder` sidecar (Xenova bge-small on CPU, EFS-backed model cache), and a one-shot `migrator` task definition. Targets an externally-managed RDS Postgres instance and an existing OIDC identity provider — neither is the module's job. --- ## What you provide before running The module deliberately stops short of creating shared infrastructure that's usually account-wide and not specific to this app. You bring: ### 1. A VPC with public + private subnets At least two of each across two AZs. Public subnets host the internet-facing ALB; private subnets host the Fargate tasks and EFS mount targets. The private subnets need outbound internet access (NAT gateway or VPC endpoints for ECR / Secrets Manager / CloudWatch / Hugging Face) so tasks can pull images, decrypt secrets, and on first cold start download the embedding model. ### 2. An RDS Postgres instance Postgres **≥ 15.5** with `pgvector`, `pg_trgm`, and `pgcrypto`. RDS makes all three available on modern versions; you may need to add them to `rds.allowed_extensions` in the parameter group, but the migrator runs `CREATE EXTENSION IF NOT EXISTS …` itself. Connectivity gotcha: the RDS security group is owned by you. After `terraform apply` you must add an inbound rule on it allowing 5432 from the module's task security groups. Use the outputs: ``` app_security_group_id # app needs RDS for runtime queries migrator_security_group_id # migrator needs RDS for DDL on apply ``` The embedder does **not** talk to Postgres. ### 3. An ACM certificate In the **same region** as the ALB (ACM certs are regional). Cover the public hostname you'll use for `domain_name`. DNS validation is the easiest route; AWS docs walk through it. ### 4. ECR repositories with pushed images The module references `var.app_image` and `var.embedder_image` by URI — it doesn't build, doesn't push, doesn't create the repos. Two repos typically: ``` shared-memory-web # built from apps/web/Dockerfile shared-memory-embedder # built from apps/embedder/Dockerfile ``` Build from the repo root and tag with whatever version scheme you prefer (git SHA, semver, etc.). The app and embedder images use unrelated runtime stacks (Node alpine vs Node slim) — keep them as separate repos. ### 5. OIDC clients Two clients in your IdP (Authentik, EntraID, Keycloak, …) — one confidential for the Web UI, one public/PKCE for the MCP endpoint. See the [main README](../README.md#oidc-provider-setup) for the Authentik walkthrough. The redirect URI you register on the Web UI client is `https://${domain_name}/api/auth/callback/oidc`, so plan the domain name *before* configuring the IdP. --- ## Quick start ```bash cd terraform/examples/basic # 1. Edit main.tf — replace vpc-…, subnet-…, ARN placeholders, image URIs. $EDITOR main.tf # 2. Create terraform.tfvars with the sensitive values (0600 perms!). umask 077 cat > terraform.tfvars < ``` ### Read its logs ```bash LOG_GROUP=$(terraform output -raw migrator_log_group_name) aws logs tail "$LOG_GROUP" --follow ``` A healthy run prints `Migrations complete.` and (if you have prior data) `Embedding backfill complete: N memories embedded.`. You should re-run the migrator after **every** deploy that ships a new SQL migration file. It's idempotent — already-applied migrations are skipped via the `_migrations` ledger table. ### Verify the app is up ```bash ALB=$(terraform output -raw alb_dns_name) curl -fsS "https://$ALB/api/health" # — once DNS / cert is wired up ``` (If DNS isn't wired yet, you can `curl --resolve memory.example.com:443:` to test against the cert without touching DNS.) --- ## Updating images Push a new tag to ECR, then re-apply with the new tag: ```bash terraform apply -var 'app_image=…/shared-memory-web:v0.5.1' ``` ECS performs a rolling deploy: `deployment_minimum_healthy_percent = 50` and `deployment_maximum_percent = 200` mean it stands up new tasks before draining old ones. If the new tasks fail their ALB health check the old ones stay. If the new image ships a SQL migration, **run the migrator again first** (or right after; the SQL is backwards-compatible in this codebase), then roll the app. The embedder side is rarer to update — the image hardly changes. When it does, EFS keeps the existing model cache so the new revision is warm immediately. --- ## DNS setup The ALB has a generated DNS name (`…elb.amazonaws.com`); you point your real hostname at it with an A-alias record. If your DNS lives in Route53: ```hcl resource "aws_route53_record" "app" { zone_id = "Z0123456789ABCDEFG" # your hosted zone name = "memory.example.com" type = "A" alias { name = module.shared_memory.alb_dns_name zone_id = module.shared_memory.alb_zone_id evaluate_target_health = true } } ``` If your DNS is elsewhere (Cloudflare, NS1, …), a CNAME from `memory.example.com` → `` works equivalently, modulo apex limitations. Once DNS propagates, the OIDC callback URL you registered earlier (`https://memory.example.com/api/auth/callback/oidc`) will start working and you can sign in. --- ## Security note Several inputs (`database_url`, `nextauth_secret`, `cli_token_secret`, `oidc_client_secret_web`) are sensitive. The module marks them as such so they're scrubbed from CLI output, but they still: - Pass through `terraform plan` and `terraform apply` - Land in `terraform.tfstate` - Round-trip through Secrets Manager versions Hardening checklist: - Put values in `terraform.tfvars` (not committed) with `chmod 600`. - Use a remote state backend with encryption (S3 + KMS) and tight IAM on the bucket. Local state in a shared repo is the failure mode. - Consider an external secret manager (1Password, Doppler, Vault) and feeding values via `-var-file` from a `terraform-data` shim. The module accepts plain strings — keep the indirection outside. - Rotate `nextauth_secret` and `cli_token_secret` periodically. Both can change with no DB migration; in-flight sessions and unexpired CLI tokens will be invalidated. The module's Secrets Manager entries are scoped under `${name_prefix}/` and the task execution role has `secretsmanager:GetSecretValue` on those ARNs only — no wildcard. --- ## What the module creates | Resource | Purpose | |---|---| | `aws_ecs_cluster` | Fargate cluster, Service Connect default namespace | | `aws_ecs_service.app` | Web/MCP service behind ALB | | `aws_ecs_service.embedder` | Internal sidecar service | | `aws_ecs_task_definition.{app,embedder,migrator}` | Task defs | | `aws_lb` + listener + target group | Public ALB, HTTPS + redirect | | `aws_efs_file_system` + access point + mount targets | Embedder model cache | | `aws_secretsmanager_secret.*` (4) | DATABASE_URL, NEXTAUTH_SECRET, CLI_TOKEN_SECRET, OIDC_CLIENT_SECRET_WEB | | `aws_cloudwatch_log_group.*` (4) | app, embedder, migrator, service-connect | | `aws_security_group.{alb,app,embedder,migrator,efs}` | Tier security groups | | `aws_iam_role.{execution,app_task,embedder_task,migrator_task}` | Execution + per-service task roles | | `aws_service_discovery_http_namespace` | Service Connect namespace `${name_prefix}.internal` | ## What the module does NOT create - VPC, subnets, NAT, route tables — you own these - RDS instance, parameter group, subnet group — you own - ACM certificate or its DNS validation records — you own - ECR repositories or the image build pipeline — you own - OIDC clients — you own - Route53 records — you own (see [DNS setup](#dns-setup)) - WAF, Shield, CloudFront — out of scope ## Module inputs See [`variables.tf`](variables.tf) for the full list with descriptions and defaults. ## Module outputs See [`outputs.tf`](outputs.tf). The ones you'll use: - `alb_dns_name`, `alb_zone_id` — for the Route53 alias - `ecs_cluster_name`, `migrator_task_definition_family`, `private_subnet_ids_for_run_task`, `migrator_security_group_id` — to assemble the `aws ecs run-task` call - `app_security_group_id` / `migrator_security_group_id` — to whitelist on your RDS SG - `app_log_group_name`, `embedder_log_group_name`, `migrator_log_group_name` — for `aws logs tail` ## Worked example See [`examples/basic/`](examples/basic/).