Skip to main content
Version: 0.4.0

Infrastructure

The service is designed to run both locally (with local SQLite/mock storage) and as a cloud-native deployment on AWS, provisioned via Terraform (terraform/).

AWS resources (terraform/main.tf)

ResourcePurpose
aws_ecr_repository.agent_serverDocker image registry for the service (image scanning enabled on push). Pinned to var.aws_region explicitly, because Lightsail can only pull from a private ECR repository in its own region.
aws_ecr_lifecycle_policy.agent_serverExpires untagged images after a day and keeps the 10 most recent tagged ones.
aws_lightsail_container_service.agentThe Lightsail container service running the agent (power nano: 0.25 vCPU / 0.5 GB, the same size as the ECS Fargate task this replaced).
aws_ecr_repository_policy.lightsail_pullGrants the Lightsail service's own pull-role principal access to the ECR repository.
aws_lightsail_container_service_deployment_version.agentThe actual running deployment: container image, environment variables, and the /health public endpoint check.
aws_cloudfront_distribution.agent_cdnCDN in front of the Lightsail service's endpoint, with caching disabled and headers forwarded to support SSE streaming. patb-cli hardcodes this CloudFront hostname, so the distribution itself is deliberately never replaced even when its origin moves.

Secrets (GitHub token, Anthropic API key, app API key) are read from AWS SSM Parameter Store at /${project_name}/${environment}/<secret_name> rather than being passed as plain Terraform variables, then passed into the container as plain environment variables — Lightsail has no equivalent of ECS's secret {} block, so they are readable by anyone holding lightsail:GetContainerServiceDeployments. The deployment consumes /${project_name}/${environment}/anthropic_api_key, github_token, and app_api_key, so all three parameters must exist before terraform apply.

Unlike the previous ECS setup, the Lightsail container is not given an IAM role for runtime AWS access — the pull-role Lightsail mints is scoped to ECR only. That means src/storage/s3.ts's default credential provider chain has nothing to resolve in this deployment, and cloud state persistence silently falls back to the in-memory mock (see Configuration and FAQ).

Variables (terraform/variables.tf)

VariableDefaultDescription
aws_regionus-east-1Primary deployment region.
project_namepinky-and-the-brain-agentsPrefix used for all resource names.
environmentdevDeployment environment name.
langchain_tracing_v2trueWhether LangSmith tracing is enabled in the deployed environment.
langchain_api_key(sensitive, no default)LangSmith API key for the deployed environment.
langchain_projectpinky-and-the-brain-agentsLangSmith project name.
image_taglatestThe container image tag the Lightsail deployment runs. Must change whenever the image changes — Terraform only sees a new deployment is needed when this string differs, and latest never does. scripts/deploy.js passes the short commit SHA (with a dirty-tree suffix when uncommitted work is deployed). The default only exists so a bare terraform plan works.

Copy terraform/terraform.tfvars.example to terraform.tfvars and fill in environment-specific values before running Terraform.

Outputs (terraform/outputs.tf)

  • ecr_repository_url - the ECR repository URL to push Docker images to.
  • lightsail_service_url - the Lightsail container service's own HTTPS endpoint. Useful to verify a deployment directly, since CloudFront's 60s origin read timeout can cut off long-running requests that Lightsail itself would have served.
  • cloudfront_service_url - the stable, public CloudFront URL for the deployed service; this is the host patb-cli targets.

Deployment flow

  1. npm run build compiles the TypeScript service.

  2. Dockerfile performs a multi-stage build, producing a minimal production image, and fails the build outright if src/storage/knowledge-store.json is missing — an agent shipped without it boots fine and answers every question from an empty vault.

  3. npm run deploy (scripts/deploy.js) applies the ECR repository first, verifies the knowledge store (chunk count and per-area breakdown) before building the image, builds and pushes the Docker image tagged with the short commit SHA, then runs the full Terraform apply. Because the image tag is immutable per build, Terraform sees a real diff and rolls the Lightsail deployment automatically — no separate force-new-deployment step is needed.

  4. npm run report-infra (scripts/report-infra.ps1) audits the live AWS infrastructure status and writes a timestamped Markdown report to infra-reports/. It is read-only - it never creates or changes resources.

    It inventories networking, compute and containers, storage and databases, application/integration services, developer tooling, global services (S3, CloudFront, Route 53, IAM) and secrets/config - roughly 52 API calls per region plus a set of account-wide calls. Each region sweep ends with a Resource Groups Tagging API call that returns every taggable resource regardless of type, so a resource can only escape the report by being both untagged and of a type the script does not describe explicitly. Anything that shows up in that catch-all without a matching section above it is the signal to add a new query.

    The script prompts for the regions to scan, listing every supported region so any number of them can be picked by index (1,3,5), range (1-4), or name (sa-east-1,us-east-1). Pressing ENTER with no selection scans all regions, which is roughly 290 AWS API calls and takes a while - the prompt warns about this before you commit to it.

    To skip the prompt, pass -Region sa-east-1,us-east-1 or -AllRegions. The script also falls back to scanning all regions when stdin is not interactive, so CI runs never hang on the prompt.

    Failed AWS calls are printed to the console and collected into a Failed AWS Calls section at the end of the report, so an empty section can be told apart from a missing IAM permission.

  5. npm run tail-logs streams logs from the running cloud container.

Provisioning from scratch

npm run create-infra (scripts/create-infra.ps1) takes an empty account to a running service. It exists because npm run deploy assumes the infrastructure is already there: deploy is the code-change path, create-infra is the from-nothing path.

The order is forced by the dependencies:

  1. Preflight - aws, terraform, npm (and docker, unless -SkipImage) on PATH, credentials that resolve, a Docker daemon that answers.
  2. SSM parameters - main.tf reads the three secrets as data sources, so Terraform errors out rather than creating them. Missing ones are seeded from .env (GITHUB_ACCESS_TOKEN, ANTHROPIC_API_KEY, AWS_APP_API_KEY, with the same fallbacks src/config.ts uses) or prompted for with hidden input. Existing parameters are never overwritten, and values never reach the console or the report.
  3. terraform init, then a targeted apply of the ECR repository alone - the Lightsail deployment points at <ecr-url>:<image_tag>, so the repository has to exist before the image and the image before the deployment.
  4. npm run build, docker build, ECR login, push.
  5. Full terraform apply - the Lightsail container service, its deployment version, and the CloudFront distribution.

Re-running is safe; every step is idempotent, so it doubles as the recovery path for a half-finished run. -PlanOnly runs terraform plan and stops. -SkipImage reuses whatever tag is already in ECR. Each run writes infra-reports/<timestamp>-create-<mode>-report.md with the per-step outcome and timing - including runs that failed, which are the interesting ones.

Account cleanup and teardown

scripts/cleanup-infra.ps1 runs in two scopes. See Infrastructure cleanup for the classification rules, safety model and deletion order.

CommandScope
npm run cleanup-infraDeletes AWS resources that are not part of this project, so the account holds nothing but pinky-and-the-brain-agents.
npm run teardown-infra-IncludeProject: deletes the project too, for a full account wipe. Rebuild with npm run create-infra.

Both are dry runs unless -Apply is passed. The dry run deletes nothing, makes only List/Describe calls, and writes its plan to infra-reports/<timestamp>-<cleanup|teardown>-dry-run-report.md.

Required IAM permissions

terraform/aws-permissions.json is a single policy covering all three scripts - the read-only inventory, the provisioning, and the deletions. Attach it to the deploying user or role.

It is one file rather than three because an AWS customer-managed policy is capped at 6,144 characters, and the earlier per-script policies came to 8,468 characters combined. Collapsing the Resource: "*" blocks and using action wildcards (ec2:Describe*, ecs:List*) brings it to roughly 3,700, well inside the limit.

Two Deny statements are the ones worth reading:

  • ProtectCallerCredentials - the caller's own IAM user and the policy granting these permissions cannot be modified or deleted. Without it, a teardown run can revoke its own access halfway through and strand the account.
  • ProtectServiceLinkedRoles - AWS-owned roles under /aws-service-role/ are off limits. They are free and AWS recreates them on demand.

There is deliberately no Deny protecting the project's own resources: the teardown mode needs to delete them, so the guard rails live in the script (dry run by default, and a typed DELETE EVERYTHING confirmation) rather than in IAM.

The penny-processor user does not currently hold this policy, so the scripts report AccessDenied for most resource types. Those failures are listed explicitly in each report's Failed AWS Calls section - a denied call is never read as "this resource type is empty", and the cleanup script never plans a deletion for a type it could not enumerate.