Skip to main content
stralos-core owns shared AWS infrastructure for service repositories. Service stacks consume core remote state rather than recreating shared networking and database resources.

Core environment stack

The environment-setup stack creates:
  • VPC.
  • Public and private subnets.
  • Internet gateway.
  • Optional NAT gateway for private subnet egress.
  • Shared application services security group.
  • RDS Postgres instance and subnet group.
  • RDS security group.
  • SSM parameters for RDS host, port, database, username, password, and connection string.

Remote state

Service repos read the core stack through the remote state key:
Environment backends use stralos-terraform-state-dev, stralos-terraform-state-test, stralos-terraform-state-staging, and stralos-terraform-state-prod. Current service state keys are region-qualified, for example au-nz/api-global/terraform.tfstate. Their backends use S3-native locking (use_lockfile = true), which requires Terraform 1.10 or later and does not use a DynamoDB lock table.

Service API infrastructure

The Activities, Documents, Estimate, Global, Rates, and Scheduler repositories define regional REST API Gateway v1 stages backed by Lambda. The stage name is main. The stacks include structured access logging, tracing where configured, region-qualified resource naming, and optional attachment to the environment’s regional Web ACL.

Global API Lambda Postgres pools

Each Lambda container creates up to three Postgres pools (owner, runtime, and platform). Without a cap, a burst of concurrent containers could exhaust the shared database. Each pool is bounded to: max: 2 — one connection for the request’s transaction and one spare so a helper that checks out while that transaction is open can queue behind nothing rather than deadlocking. connectionTimeoutMillis: 5,000 — a saturated database fails the checkout in 5 seconds. The safe error handler turns that into a 503 Service Unavailable with Retry-After: 5 rather than hanging until API Gateway times out at 29 seconds. These bounds apply only to the deployed Lambda entrypoint (src/lambda.ts). The local development server and migration scripts keep pg’s defaults (max: 10, no connection timeout) because they serve concurrent requests from a single process where a small pool would serialise them.

Global API Lambda alarms

stralos-api-global carries CloudWatch alarm coverage for its Lambda plane. Every deployed function has an Errors alarm and a Throttles alarm, both using treat_missing_data = "notBreaching" so an idle period never false-alarms. The HTTP entrypoint (api) additionally has a guarded error-rate alarm (Errors/Invocations via metric math, only evaluated at 10 or more invocations in a 5-minute window) to catch a proportional burst even where the absolute Errors threshold is set higher to tolerate transient 5xx responses. Scheduled workers — the lifecycle sweep (rate(10 minutes)), the directory-flush drain (rate(1 minute)) and its daily reconcile (rate(1 day)), the report-flush drain (rate(1 minute)) and its daily redrive (rate(1 day)), and the estate event-flush drain (rate(1 minute)) — each carry a dead-man’s-switch alarm on AWS/Events SuccessfulInvocationAttempts scoped to the EventBridge rule. These alarms use treat_missing_data = "breaching" because absence of the metric is the failure. Window sizes match each rule’s cadence to avoid false-alarms on healthy idle intervals. The estate event-flush Lambda additionally raises an event_outbox_backlog alarm when any row in the event_outbox table has been unsent for more than 15 minutes. That condition indicates a publishing failure rather than normal latency: the inline after-commit publish sends almost everything within the request, and the flusher retries each minute. Likely causes are a missing or incorrect EVENT_BUS_NAME, an IAM AccessDenied on events:PutEvents, or the bus not existing. A missing EVENT_BUS_NAME is non-fatal — the event is still written to event_outbox and the create still succeeds — but no events leave the outbox until the variable is set. The lifecycle sweep Lambda’s reserved_concurrent_executions is excluded from Terraform management via a lifecycle { ignore_changes = [reserved_concurrent_executions] } block. This preserves any operator-set concurrency gate across a terraform apply. Set reserved_concurrent_executions = 0 on the function to pause the sweep during a coordinated cutover (such as the project membership migration), and restore it manually once the new worker and schema are verified. Terraform will not reset the gate on the next apply. The API Gateway stage has its own 5XXError alarm. Fastify returns application errors as successful Lambda invocations, so the Lambda Errors alarm cannot see them. The gateway-level alarm closes that gap. All alarms fire the shared SNS alarm topic (var.lambda_alarm_sns_topic_arn) created and managed by stralos-admin-api. A Terraform check compares the alarm map with the Lambda functions already live in AWS and reports a non-blocking plan warning for a gap. A function introduced by the current apply is detectable only on a later plan.

Documents infrastructure

The Documents service adds its own S3 bucket, EventBridge/SQS routing, worker Lambdas, DLQs, provider permissions, and alarms. Its API uses the shared REST stage and optional Web ACL association. Documents resource names are region-qualified. The bucket and EventBridge bus default to {project}-{environment}-{region_key}-documents; stage queues use the corresponding {project}-{environment}-{region_key}-api-documents-<stage> prefix. region_key (default au-nz) also selects the workload AWS region through aws_region_by_key. The bucket is versioned, encrypted with SSE-S3, publishes object-created events to EventBridge, and aborts incomplete multipart uploads. Its CORS rule permits browser presigned-POST uploads from the cors_allow_origins list; an empty list falls back to http://localhost:5173. The bucket carries several S3 lifecycle rules:
  • abort-incomplete-multipart — aborts incomplete multipart uploads after 1 day.
  • expire-quarantine — expires infected objects in quarantine/ after 30 days.
  • expire-sync-staging — expires sync-staging objects and their noncurrent versions after 7 days.
  • expire-sync-staging-delete-markers — reaps expired delete markers in sync-staging/.
  • expire-noncurrent-versions (bucket-wide) — expires noncurrent versions of every object after 30 days. This is the retention half of document deletion: DELETE /documents/:id issues a bare DeleteObject, which writes a delete marker and leaves the upload as a noncurrent version; this rule reaps those bytes after the 30-day recovery window.
  • expire-delete-markers (bucket-wide) — reaps expired delete markers once their last noncurrent version has been removed, preventing indefinite accumulation of one marker per deleted document.
The expire-noncurrent-versions rule is intentionally bucket-wide rather than prefix-scoped. A prefix list would silently exclude any prefix added later. Adding this rule can only shorten retention, never lengthen it: every prefix either already had a shorter noncurrent bound and keeps it, or had none and gains the 30-day cap. Each primary pipeline-stage worker has an SQS event-source mapping. Terraform emits scaling_config only when that stage’s maximum_concurrency is between 2 and 1,000; -1 omits the cap. The current defaults cap parse-jobs, parse-results, summary-jobs, classify-jobs, and variation-assessment-jobs at 5 concurrent executions, and area-jobs at 10. upload-events and scan-results remain uncapped. Primary worker Lambda functions and their corresponding IAM roles share a capped name. Terraform keeps {name_prefix}-worker-{stage} when it is at most 64 characters. For a longer value, it keeps the first 55 characters and appends - plus the first eight hexadecimal characters of the full name’s SHA-1 hash, producing a stable unique name within the AWS limit.