diff --git a/README.md b/README.md index 0f533e6..09f23ff 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ memories are scoped per user. ## Prerequisites -- A host with **Docker** and **Docker Compose v2** installed. +- A host with **Docker** and **Docker Compose v2** installed (≥ 2.24.0 if you + plan to use the [external Postgres override](#external-postgres-rds-cloud-sql-etc)). - An **OIDC identity provider** you control (Authentik, EntraID, Keycloak, Okta, Auth0, Zitadel, …). The setup walkthrough below uses Authentik because that's what we run; other IdPs need equivalent settings. diff --git a/docker-compose.external-db.yml b/docker-compose.external-db.yml index bc107af..76a4f52 100644 --- a/docker-compose.external-db.yml +++ b/docker-compose.external-db.yml @@ -18,6 +18,13 @@ # The DB user needs privileges to `CREATE EXTENSION` for pgvector, pg_trgm, # and pgcrypto on first run — on RDS that means the `rds_superuser` role, or # pre-create the extensions yourself. See README "External Postgres". +# +# REQUIRES DOCKER COMPOSE >= 2.24.0 (Docker Desktop >= 4.25, or Compose plugin +# 2.24.0+). The `!override` YAML tag on the depends_on blocks below is what +# fully replaces — rather than merges — the base file's `depends_on: db` +# entries. On older Compose the tag is silently ignored, the `db` dependency +# survives the merge, and startup fails with "depends on undefined service +# db". Check with: docker compose version # ============================================================================= services: diff --git a/terraform/ecs.tf b/terraform/ecs.tf index 5555c2a..39a3067 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -61,6 +61,7 @@ locals { { name = "EMBEDDER_URL", value = "http://embedder:${local.embedder_port}" }, { name = "EMBEDDING_MODEL", value = var.embedding_model }, { name = "EMBEDDING_DIM", value = tostring(var.embedding_dim) }, + { name = "NEXT_TELEMETRY_DISABLED", value = "1" }, ] # `secrets` block format that ECS expects: name = env-var name, valueFrom @@ -73,6 +74,12 @@ locals { ] embedder_environment = [ + # awsvpc network mode gives every task its own ENI — bind to 0.0.0.0 + # explicitly so Service Connect reaches the embedder on the task's + # ENI address. Default Node servers often bind 127.0.0.1, which + # would silently make every app→embedder call time out. + { name = "HOST", value = "0.0.0.0" }, + { name = "PORT", value = tostring(local.embedder_port) }, { name = "LOG_LEVEL", value = var.log_level }, { name = "EMBEDDING_MODEL", value = var.embedding_model }, { name = "EMBEDDING_DIM", value = tostring(var.embedding_dim) }, @@ -272,7 +279,7 @@ resource "aws_ecs_service" "embedder" { task_definition = aws_ecs_task_definition.embedder.arn desired_count = var.embedder_desired_count launch_type = "FARGATE" - enable_execute_command = true + enable_execute_command = var.enable_execute_command network_configuration { subnets = var.private_subnet_ids @@ -322,7 +329,7 @@ resource "aws_ecs_service" "app" { task_definition = aws_ecs_task_definition.app.arn desired_count = var.app_desired_count launch_type = "FARGATE" - enable_execute_command = true + enable_execute_command = var.enable_execute_command network_configuration { subnets = var.private_subnet_ids diff --git a/terraform/examples/basic/outputs.tf b/terraform/examples/basic/outputs.tf index 56d9876..35f6cd2 100644 --- a/terraform/examples/basic/outputs.tf +++ b/terraform/examples/basic/outputs.tf @@ -51,4 +51,5 @@ output "migrator_log_group_name" { output "secret_arns" { description = "Visibility into where the module stored its secrets." value = module.shared_memory.secret_arns + sensitive = true } diff --git a/terraform/iam.tf b/terraform/iam.tf index ef922d7..dc12f51 100644 --- a/terraform/iam.tf +++ b/terraform/iam.tf @@ -67,3 +67,38 @@ resource "aws_iam_role" "migrator_task" { assume_role_policy = data.aws_iam_policy_document.ecs_tasks_assume.json tags = local.tags } + +# ---- ECS Execute Command (opt-in via var.enable_execute_command) ---- +# +# When the operator flips this on for incident debugging, the task role +# needs the SSM messages permissions for the channel to open. We attach +# the policy conditionally to both app and embedder task roles — the +# migrator is short-lived and doesn't get exec. + +data "aws_iam_policy_document" "exec_command" { + count = var.enable_execute_command ? 1 : 0 + statement { + sid = "AllowECSExecuteCommand" + actions = [ + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel", + ] + resources = ["*"] + } +} + +resource "aws_iam_role_policy" "app_exec_command" { + count = var.enable_execute_command ? 1 : 0 + name = "${var.name_prefix}-app-exec-command" + role = aws_iam_role.app_task.id + policy = data.aws_iam_policy_document.exec_command[0].json +} + +resource "aws_iam_role_policy" "embedder_exec_command" { + count = var.enable_execute_command ? 1 : 0 + name = "${var.name_prefix}-embedder-exec-command" + role = aws_iam_role.embedder_task.id + policy = data.aws_iam_policy_document.exec_command[0].json +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf index c616125..486d945 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -84,6 +84,7 @@ output "private_subnet_ids_for_run_task" { } output "secret_arns" { - description = "Map of env-var name to Secrets Manager ARN. For visibility only — do not re-feed back into the module." + description = "Map of env-var name to Secrets Manager ARN. For visibility only — do not re-feed back into the module. Marked sensitive so the secret names don't print in `terraform apply` stdout or CI logs." value = local.secret_arns + sensitive = true } diff --git a/terraform/variables.tf b/terraform/variables.tf index 36fd728..48b3e13 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -190,6 +190,22 @@ variable "log_retention_days" { default = 14 } +variable "enable_execute_command" { + description = <<-EOT + Enable AWS ECS Execute Command on the app + embedder services. When true, + operators with the appropriate IAM permission can `aws ecs execute-command` + into a running task — useful for debugging, dangerous as a standing + posture (any principal with `ecs:ExecuteCommand` on these services gets a + shell inside the container). Defaults `false`. Flip to `true` for an + incident, then back to `false` and re-apply when done. + + When enabled, the module also attaches the SSM messages policy to both + task roles so the feature actually works. + EOT + type = bool + default = false +} + variable "tags" { description = "Tags merged onto every resource the module creates." type = map(string)