Skip to content

feat(devtools): Aurora Serverless v2 scale-to-zero + NAT-free Lambda connectivity - #640

Open
seanspeaks wants to merge 2 commits into
nextfrom
claude/aurora-serverless-scale-to-zero-nat-free
Open

feat(devtools): Aurora Serverless v2 scale-to-zero + NAT-free Lambda connectivity#640
seanspeaks wants to merge 2 commits into
nextfrom
claude/aurora-serverless-scale-to-zero-nat-free

Conversation

@seanspeaks

@seanspeaks seanspeaks commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements ADR-033. Adds two independent, opt-in, default-off capabilities on database.postgres so a Frigg-owned Aurora Serverless v2 cluster can idle at ~$0 instead of the current ~$75/mo floor (0.5 ACU always-on + an always-on NAT Gateway). Existing app definitions compose a byte-for-byte identical template.

1. Scale-to-zero (minCapacity: 0)

  • Validator accepts minCapacity of 0 (Aurora Serverless v2 scale-to-zero, GA Nov 2024) or [0.5, 128]; (0, 0.5) remains invalid. Also cross-checks minCapacity <= maxCapacity.
  • Scaling config uses nullish coalescing — MinCapacity: dbConfig.minCapacity ?? 0.5 / MaxCapacity: dbConfig.maxCapacity ?? 4 (the old || 0.5 turned a requested 0 back into 0.5).
  • When minCapacity === 0, emits SecondsUntilAutoPause from a new optional secondsUntilAutoPause (default 300, valid 300–86400; a warning fires if it's set while minCapacity !== 0).
  • Warns (not fails) if minCapacity: 0 is pinned to an engine older than Aurora PG 13.15/14.12/15.7/16.3. The default 15.13 qualifies.

2. Connectivity mode (connectivity: 'vpc' default | 'public')

  • 'vpc' (default, unchanged): Aurora in private subnets, Lambda attached to the VPC, ingress from the Lambda SG.
  • 'public' (NAT-free): Aurora in public subnets with PubliclyAccessible: true; ingress opens 5432 to a new allowedCidrs option (default ['0.0.0.0/0']) via CidrIpone ingress rule per CIDR — instead of SourceSecurityGroupId; DATABASE_URL carries sslmode=require; the Lambda is left OUT of the VPC (no NAT Gateway, no VPC endpoints) but the public subnets still get an Internet Gateway default route + associations so the endpoint is reachable; VPC_ENABLED=false. Requires vpc.enable: true (validated).
  • Both combine: connectivity: 'public' + minCapacity: 0 → a Frigg-owned Aurora that idles at $0 with no NAT.

The decoupling seam: the composer sets provider.vpc only when VpcBuilder returns a truthy vpcConfig (infrastructure-composer.js:126). In public mode the VpcBuilder still builds the VPC / public subnets / DB subnet group / IGW public routing Aurora requires, but skips the NAT Gateway + VPC endpoints and sets result.vpcConfig = null — so provider.vpc stays unset and the Lambda is never VPC-attached. The public-subnet IGW routing is emitted only for a Frigg-created (stack) VPC (guarded by FriggInternetGateway presence); a discovered/existing VPC is left alone.

Files changed

  • packages/devtools/infrastructure/domains/database/aurora-builder.js — validator (minCapacity 0, min≤max, secondsUntilAutoPause + ignore-warning, connectivity, allowedCidrs numeric validation, empty-in-public error, public+vpc.enable rule, discover/use-existing warnings, engine warning), scaling config, public-mode subnet/PubliclyAccessible selection, per-CIDR CidrIp ingress, TLS in DATABASE_URL (create-new/discover/use-existing paths), config-mirror translation.
  • packages/devtools/infrastructure/domains/networking/vpc-builder.js — public mode: emit IGW public-subnet routing (stack VPC only), skip NAT Gateway + VPC endpoints, clear vpcConfig, set VPC_ENABLED=false.
  • packages/devtools/infrastructure/domains/shared/types/app-definition.js — documents the four new fields; fixes stale maxCapacity default JSDoc (1 → 4).
  • packages/devtools/infrastructure/domains/database/aurora-builder.test.js — updated the minCapacity error-message assertion.
  • packages/devtools/infrastructure/__tests__/aurora-scale-to-zero-connectivity.test.js — new template-shape + validator tests.
  • docs/architecture-decisions/033-...md — the ADR.

Tests

./node_modules/.bin/jest infrastructure/__tests__/aurora-scale-to-zero-connectivity.test.js infrastructure/domains/database/aurora-builder.test.js from packages/devtools: 76 passed / 76 (2 suites).

  • Validator: minCapacity 0/0.3/0.5/64, min>max, secondsUntilAutoPause range + ignore-warning, connectivity enum, allowedCidrs numeric-range + empty-in-public error, public-requires-vpc, discover/use-existing warnings, engine warning.
  • Scale-to-zero (template): MinCapacity === 0 (mutation-checked .not.toBe(0.5)), SecondsUntilAutoPause 300/custom, MaxCapacity 4.
  • Public connectivity (template): per-CIDR CidrIp ingress (default 0.0.0.0/0, no SourceSecurityGroupId), PubliclyAccessible: true in public subnets, no NAT Gateway, IGW public route + route table + both subnet associations present (no NAT route), provider.vpc unset, VPC_ENABLED=false, sslmode=require, combined $0-idle path.
  • No-regression (vpc default): MinCapacity 0.5 / no SecondsUntilAutoPause, ingress via SourceSecurityGroupId (FriggAuroraIngressRule unchanged), not publicly accessible, provider.vpc set, VPC_ENABLED=true, no sslmode.

Mutation-verified: ?? 0.5|| 0.5, removing vpcConfig = null, removing createPublicRouting, and forcing VPC_ENABLED='true' each break the intended test(s).

Review findings — status

Fixed: public-subnet IGW routing decoupled from NAT (HIGH); empty allowedCidrs in public mode now an error (SEC); stale aurora-builder.test.js assertion; discover/use-existing public warning + useExistingAurora TLS; minCapacity <= maxCapacity; connectivity:'public' requires vpc.enable; secondsUntilAutoPause ignored-warning; VPC_ENABLED=false in public mode; numeric IPv4 CIDR validation (documented IPv4-only); corrected the .config.* translation comment.

Deferred / accepted (per reviewer): sslmode=require (not verify-full) is ADR-acceptable; the FriggLambdaSecurityGroup/vpcSecurityGroupIds coupling is correct today. IPv6 (CidrIpv6) is explicitly out of scope for allowedCidrs (documented).

Note: end-to-end AWS deployment was not validated (needs a live account) — template shape only. The public-subnet IGW routing and the discover/use-existing public path in particular are argued from CloudFormation semantics + unit tests, not a live deploy. vpc-builder.test.js has 8 pre-existing failures on next (unrelated base-branch red) — unchanged by this PR (verified before/after). The repo's full "Frigg CI" is also independently red because prisma:generate isn't run before the infra tests; the new tests run without Prisma generation or a live AWS account.

🤖 Generated with Claude Code


Generated by Claude Code

📦 Published PR as canary version: 2.0.0--canary.640.5140601.0

✨ Test out this PR locally via:

npm install @friggframework/admin-scripts@2.0.0--canary.640.5140601.0
npm install @friggframework/core@2.0.0--canary.640.5140601.0
npm install @friggframework/devtools@2.0.0--canary.640.5140601.0
npm install @friggframework/eslint-config@2.0.0--canary.640.5140601.0
npm install @friggframework/prettier-config@2.0.0--canary.640.5140601.0
npm install @friggframework/schemas@2.0.0--canary.640.5140601.0
npm install @friggframework/serverless-plugin@2.0.0--canary.640.5140601.0
npm install @friggframework/test@2.0.0--canary.640.5140601.0
npm install @friggframework/ui@2.0.0--canary.640.5140601.0
# or 
yarn add @friggframework/admin-scripts@2.0.0--canary.640.5140601.0
yarn add @friggframework/core@2.0.0--canary.640.5140601.0
yarn add @friggframework/devtools@2.0.0--canary.640.5140601.0
yarn add @friggframework/eslint-config@2.0.0--canary.640.5140601.0
yarn add @friggframework/prettier-config@2.0.0--canary.640.5140601.0
yarn add @friggframework/schemas@2.0.0--canary.640.5140601.0
yarn add @friggframework/serverless-plugin@2.0.0--canary.640.5140601.0
yarn add @friggframework/test@2.0.0--canary.640.5140601.0
yarn add @friggframework/ui@2.0.0--canary.640.5140601.0

…connectivity

Implements ADR-033. Adds two independent, opt-in, default-off capabilities on
database.postgres so a Frigg-owned Aurora can idle at ~$0.

Scale-to-zero (minCapacity: 0):
- Validator accepts minCapacity 0 OR [0.5,128]; (0,0.5) rejected.
- Scaling config uses `?? 0.5` / `?? 4` (the old `|| 0.5` turned a requested 0
  back into 0.5) and emits SecondsUntilAutoPause (default 300, range 300-86400)
  when minCapacity === 0.
- Warns when minCapacity:0 is pinned to an engine older than 13.15/14.12/15.7/16.3.

Connectivity mode (connectivity: 'vpc' default | 'public'):
- 'public' places Aurora in public subnets (PubliclyAccessible), opens 5432 to
  allowedCidrs (default ['0.0.0.0/0']) via CidrIp — one ingress rule per CIDR —
  instead of the Lambda security group, and enforces TLS (sslmode=require).
- The VpcBuilder decouples the Lambda VPC attachment + NAT from Aurora's
  networking: it still builds the VPC/public subnets/DB subnet group Aurora needs
  but skips the NAT Gateway and VPC endpoints and clears vpcConfig, so the
  composer leaves provider.vpc unset and the Lambda keeps default internet egress.

Also threads the new fields through the legacy-config translation, documents them
in app-definition types, and fixes the stale maxCapacity default JSDoc (1 -> 4).

Default behavior is unchanged: 'vpc' connectivity + minCapacity 0.5 compose a
byte-for-byte identical template. Adds template-shape unit tests (validator +
generated CloudFormation) covering both capabilities and the no-regression path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDh45c1vm91ySYtvVv9Z65
@netlify

netlify Bot commented Aug 20, 2026

Copy link
Copy Markdown

Deploy Preview for friggframework-org canceled.

Name Link
🔨 Latest commit 5140601
🔍 Latest deploy log https://app.netlify.com/projects/friggframework-org/deploys/6a86872f62e3fc0008d02c86

@seanspeaks seanspeaks added release Create a release when this pr is merged prerelease This change is available in a prerelease. labels Aug 20, 2026 — with Claude
Comment on lines +634 to +637
const allowedCidrs =
Array.isArray(dbConfig.allowedCidrs) && dbConfig.allowedCidrs.length > 0
? dbConfig.allowedCidrs
: ['0.0.0.0/0'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security issue: Empty array allowedCidrs: [] is treated as missing and defaults to ['0.0.0.0/0'], opening the database to the entire internet when the user may have intended to block all access.

const allowedCidrs =
    Array.isArray(dbConfig.allowedCidrs) && dbConfig.allowedCidrs.length > 0
        ? dbConfig.allowedCidrs
        : ['0.0.0.0/0'];

If a user explicitly sets allowedCidrs: [], this condition evaluates to false and falls through to the default ['0.0.0.0/0'], creating a wide-open security rule instead of no rules.

Fix: Either validate that allowedCidrs cannot be empty in the validator, or handle empty arrays explicitly to create no ingress rules (though that would make the database unreachable).

Suggested change
const allowedCidrs =
Array.isArray(dbConfig.allowedCidrs) && dbConfig.allowedCidrs.length > 0
? dbConfig.allowedCidrs
: ['0.0.0.0/0'];
const allowedCidrs =
Array.isArray(dbConfig.allowedCidrs)
? dbConfig.allowedCidrs
: ['0.0.0.0/0'];

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

…tor hardening

Adversarial review follow-ups on the Aurora scale-to-zero / NAT-free work:

- [HIGH] Public subnets are now internet-routable. Skipping NAT dropped the
  IGW default route + subnet associations (they were only emitted as a side
  effect of the NAT build), leaving the public Aurora endpoint unreachable. In
  public mode the VpcBuilder now calls createPublicRouting for a Frigg-created
  (stack) VPC — guarded by the presence of FriggInternetGateway so a
  discovered/existing VPC (whose subnets already route to an IGW) is left alone.
- [SEC] Empty allowedCidrs with connectivity=public is now a validation ERROR
  instead of silently falling back to 0.0.0.0/0.
- Fixed the stale aurora-builder.test.js assertion for the new minCapacity error.
- Added a loud warning for connectivity=public with discover/use-existing (Frigg
  can't move an existing cluster to public subnets, and the Lambda is detached);
  useExistingAurora now applies sslmode=require in public mode.
- Added minCapacity <= maxCapacity cross-check.
- connectivity=public now requires vpc.enable=true (clear error, not a downstream
  "requires 2 public subnets" throw).
- Warn when secondsUntilAutoPause is set with minCapacity !== 0 (ignored).
- VPC_ENABLED is 'false' in public mode so /health doesn't misreport isInVpc.
- Tightened allowedCidrs validation to real IPv4 numeric ranges (octets <=255,
  prefix <=32); documented IPv4-only.
- Corrected the .config.* translation comment (it is a mirror, not the
  load-bearing 0-propagation path).

Default vpc path unchanged. New + existing aurora-builder tests green;
vpc-builder.test.js failures are pre-existing base-branch red (unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDh45c1vm91ySYtvVv9Z65
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prerelease This change is available in a prerelease. release Create a release when this pr is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants