@fjall/components-infrastructure 2.31.1 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/app.d.ts +27 -1
- package/dist/lib/app.js +35 -4
- package/dist/lib/patterns/aws/database.d.ts +19 -1
- package/dist/lib/patterns/aws/database.js +22 -3
- package/dist/lib/patterns/aws/devSubstrate.d.ts +85 -0
- package/dist/lib/patterns/aws/devSubstrate.js +390 -0
- package/dist/lib/patterns/aws/devSubstrate.waker.source.cjs +13 -0
- package/dist/lib/resources/aws/compute/applicationLoadBalancer.d.ts +17 -0
- package/dist/lib/resources/aws/compute/applicationLoadBalancer.js +23 -0
- package/dist/lib/resources/aws/compute/ecsNetworking.d.ts +4 -10
- package/dist/lib/resources/aws/compute/ecsNetworking.js +13 -48
- package/dist/lib/resources/aws/compute/ecsRoles.d.ts +25 -0
- package/dist/lib/resources/aws/compute/ecsRoles.js +39 -20
- package/dist/lib/resources/aws/compute/hostHeaderListenerRule.d.ts +23 -0
- package/dist/lib/resources/aws/compute/hostHeaderListenerRule.js +59 -0
- package/dist/lib/resources/aws/compute/lambdaAlbTarget.d.ts +17 -0
- package/dist/lib/resources/aws/compute/lambdaAlbTarget.js +18 -0
- package/dist/lib/resources/aws/compute/listenerRouting.d.ts +21 -0
- package/dist/lib/resources/aws/compute/listenerRouting.js +20 -0
- package/dist/lib/resources/aws/database/rdsAurora.d.ts +16 -0
- package/dist/lib/resources/aws/database/rdsAurora.js +57 -3
- package/dist/lib/resources/aws/database/rdsDefaults.d.ts +28 -0
- package/dist/lib/resources/aws/database/rdsDefaults.js +28 -0
- package/dist/lib/utils/devSubstrateTags.d.ts +22 -0
- package/dist/lib/utils/devSubstrateTags.js +22 -0
- package/package.json +5 -5
|
@@ -2,12 +2,22 @@ import { Stack } from "aws-cdk-lib";
|
|
|
2
2
|
import { Effect, Policy, PolicyStatement, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
3
3
|
import { deriveSsmSecretsPath } from "./ecsTaskDefinition.js";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Builds the base ECS task-execution role shared by every task the platform
|
|
6
|
+
* runs: the ECR-pull quartet (account-level, so `resources: ["*"]`) and a
|
|
7
|
+
* ViaService-conditioned `kms:Decrypt` for SSM SecureString / Secrets-Manager
|
|
8
|
+
* CMK env injection. Pass `ssmSecretsPath` (already derived — no partition/
|
|
9
|
+
* region/account prefix) to add the path-scoped `ssm:GetParameters` grant.
|
|
10
|
+
* Trust principal is `ecs-tasks.amazonaws.com`.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately omits `logs:*` and `secretsmanager:GetSecretValue`: on the
|
|
13
|
+
* ECS-pattern path `AwsLogDriver.bind()` and `addContainer({ secrets })`
|
|
14
|
+
* auto-grant those on the exact LogGroup / secret ARNs. A caller that builds a
|
|
15
|
+
* SHARED or IMPORTED role those auto-grants cannot reach — e.g. the dev
|
|
16
|
+
* substrate's slot exec role, referenced by slot task-defs via ARN — MUST add
|
|
17
|
+
* the explicit `logs` / `secretsmanager` statements itself.
|
|
8
18
|
*/
|
|
9
|
-
export function
|
|
10
|
-
const executionRole = new Role(
|
|
19
|
+
export function createBaseExecutionRole(scope, id, opts = {}) {
|
|
20
|
+
const executionRole = new Role(scope, id, {
|
|
11
21
|
assumedBy: new ServicePrincipal("ecs-tasks.amazonaws.com")
|
|
12
22
|
});
|
|
13
23
|
// GetAuthorizationToken is an account-level API that requires resources: ["*"].
|
|
@@ -23,25 +33,13 @@ export function createExecutionRole(ctx, serviceName) {
|
|
|
23
33
|
],
|
|
24
34
|
resources: ["*"]
|
|
25
35
|
}));
|
|
26
|
-
const { partition, region, account } = Stack.of(
|
|
27
|
-
|
|
28
|
-
// binds an explicit LogGroup construct (CFN auto-named — there is NO
|
|
29
|
-
// `/ecs/{cluster}/...` group; that shape is only the stream prefix), and
|
|
30
|
-
// `AwsLogDriver.bind()` auto-grants CreateLogStream/PutLogEvents on the
|
|
31
|
-
// real group's ARN. A hand-rolled `/ecs/{cluster}*` statement grants
|
|
32
|
-
// access to log groups that do not exist.
|
|
33
|
-
// Gotcha: do NOT add a manual `secretsImport` grant here. `addContainer({ secrets })`
|
|
34
|
-
// auto-grants `secret.grantRead(executionRole)` on the resolved complete ARN (exact, no
|
|
35
|
-
// wildcard). A manual bare/`-*` statement is the 2026-06-04 outage shape and is dead.
|
|
36
|
-
const serviceProps = ctx.props.services.find((s) => s.name === serviceName);
|
|
37
|
-
const hasSsmSecrets = serviceProps?.containers.some((container) => container.secrets && container.secrets.length > 0) ?? false;
|
|
38
|
-
if (hasSsmSecrets && serviceProps) {
|
|
39
|
-
const ssmPath = deriveSsmSecretsPath(ctx.props, serviceName, serviceProps.ssmSecretsPath);
|
|
36
|
+
const { partition, region, account } = Stack.of(scope);
|
|
37
|
+
if (opts.ssmSecretsPath !== undefined) {
|
|
40
38
|
executionRole.addToPolicy(new PolicyStatement({
|
|
41
39
|
effect: Effect.ALLOW,
|
|
42
40
|
actions: ["ssm:GetParameters", "ssm:GetParameter"],
|
|
43
41
|
resources: [
|
|
44
|
-
`arn:${partition}:ssm:${region}:${account}:parameter${
|
|
42
|
+
`arn:${partition}:ssm:${region}:${account}:parameter${opts.ssmSecretsPath}/*`
|
|
45
43
|
]
|
|
46
44
|
}));
|
|
47
45
|
}
|
|
@@ -61,6 +59,27 @@ export function createExecutionRole(ctx, serviceName) {
|
|
|
61
59
|
}));
|
|
62
60
|
return executionRole;
|
|
63
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Creates the execution role for ECS infrastructure operations.
|
|
64
|
+
* Used by the ECS agent to pull images, write logs, and inject secrets.
|
|
65
|
+
* NOT used by application code — that's the task role.
|
|
66
|
+
*
|
|
67
|
+
* Delegates the ECR/kms/ssm shape to `createBaseExecutionRole`. Logs +
|
|
68
|
+
* secretsmanager are auto-granted downstream on this path (AwsLogDriver.bind()
|
|
69
|
+
* on the real LogGroup, addContainer({ secrets }) on the exact secret ARN), so a
|
|
70
|
+
* hand-rolled `/ecs/{cluster}*` logs or bare `-*` secrets statement here would be
|
|
71
|
+
* dead (the 2026-06-04 outage shape).
|
|
72
|
+
*/
|
|
73
|
+
export function createExecutionRole(ctx, serviceName) {
|
|
74
|
+
const serviceProps = ctx.props.services.find((s) => s.name === serviceName);
|
|
75
|
+
const hasSsmSecrets = serviceProps?.containers.some((container) => container.secrets && container.secrets.length > 0) ?? false;
|
|
76
|
+
const ssmSecretsPath = hasSsmSecrets && serviceProps
|
|
77
|
+
? deriveSsmSecretsPath(ctx.props, serviceName, serviceProps.ssmSecretsPath)
|
|
78
|
+
: undefined;
|
|
79
|
+
return createBaseExecutionRole(ctx.scope, `${serviceName}ExecutionRole`, {
|
|
80
|
+
ssmSecretsPath
|
|
81
|
+
});
|
|
82
|
+
}
|
|
64
83
|
/**
|
|
65
84
|
* Creates the task role for application code running in the container.
|
|
66
85
|
* This role is assumed by the application, not the ECS agent.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ListenerCondition } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
2
|
+
import type { EcsRoutingConfig } from "./ecsTypes.js";
|
|
3
|
+
/**
|
|
4
|
+
* Mutable ALB rule-priority state shared across every rule on one listener. The
|
|
5
|
+
* ECS cluster path and the dev substrate both thread a single instance so
|
|
6
|
+
* auto-incremented and host-hashed priorities never collide on the same listener.
|
|
7
|
+
*/
|
|
8
|
+
export interface PriorityState {
|
|
9
|
+
nextPriority: number;
|
|
10
|
+
usedPriorities: Set<number>;
|
|
11
|
+
}
|
|
12
|
+
/** Returns the next unused auto-incremented ALB priority, skipping any manually assigned values. */
|
|
13
|
+
export declare function getNextPriority(state: PriorityState): number;
|
|
14
|
+
export declare function buildRoutingConditions(rule: EcsRoutingConfig | undefined): ListenerCondition[];
|
|
15
|
+
/**
|
|
16
|
+
* A deterministic, in-band ALB rule priority for a host-routed slot rule. Same
|
|
17
|
+
* host → same priority across synths. Collisions (two hosts hashing equal, a
|
|
18
|
+
* host re-registering, or a manually-claimed priority) are resolved by
|
|
19
|
+
* open-addressing linear probe with wraparound within the band, claimed against
|
|
20
|
+
* the shared `usedPriorities`. Throws only if the (40 000-slot) band is exhausted
|
|
21
|
+
* — unreachable in practice given AWS's per-listener rule ceiling.
|
|
22
|
+
*/
|
|
23
|
+
export declare function deterministicHostPriority(host: string, state: PriorityState): number;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ListenerCondition } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
2
|
+
/** Returns the next unused auto-incremented ALB priority, skipping any manually assigned values. */
|
|
3
|
+
export function getNextPriority(state) {
|
|
4
|
+
while (state.usedPriorities.has(state.nextPriority)) {
|
|
5
|
+
state.nextPriority++;
|
|
6
|
+
}
|
|
7
|
+
const priority = state.nextPriority++;
|
|
8
|
+
state.usedPriorities.add(priority);
|
|
9
|
+
return priority;
|
|
10
|
+
}
|
|
11
|
+
export function buildRoutingConditions(rule) {
|
|
12
|
+
const conditions = [];
|
|
13
|
+
if (rule?.path) {
|
|
14
|
+
conditions.push(ListenerCondition.pathPatterns([rule.path]));
|
|
15
|
+
}
|
|
16
|
+
if (rule?.host) {
|
|
17
|
+
conditions.push(ListenerCondition.hostHeaders([rule.host]));
|
|
18
|
+
}
|
|
19
|
+
return conditions;
|
|
20
|
+
}
|
|
21
|
+
// ALB rule priorities are 1..50000 and unique per listener. Host-hashed slot
|
|
22
|
+
// rules occupy a band DISJOINT from the auto-increment counter (which starts at
|
|
23
|
+
// 100 and climbs — realistically a few hundred at most), so a deterministic slot
|
|
24
|
+
// priority can never collide with an ECS-service rule that shares the same
|
|
25
|
+
// PriorityState. Band [10000, 49999] sits above the counter and below the 50000 max.
|
|
26
|
+
const HOST_PRIORITY_BAND_START = 10_000;
|
|
27
|
+
const HOST_PRIORITY_BAND_SIZE = 40_000;
|
|
28
|
+
/**
|
|
29
|
+
* FNV-1a 32-bit string hash. Deterministic across processes and machines (no
|
|
30
|
+
* Math.random / Date), so the same host maps to the same base priority on every
|
|
31
|
+
* synth — stable rule ordering, no spurious CloudFormation diffs.
|
|
32
|
+
*/
|
|
33
|
+
function fnv1a32(input) {
|
|
34
|
+
let hash = 0x811c9dc5;
|
|
35
|
+
for (let i = 0; i < input.length; i++) {
|
|
36
|
+
hash ^= input.charCodeAt(i);
|
|
37
|
+
hash = Math.imul(hash, 0x0100_0193);
|
|
38
|
+
}
|
|
39
|
+
return hash >>> 0;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A deterministic, in-band ALB rule priority for a host-routed slot rule. Same
|
|
43
|
+
* host → same priority across synths. Collisions (two hosts hashing equal, a
|
|
44
|
+
* host re-registering, or a manually-claimed priority) are resolved by
|
|
45
|
+
* open-addressing linear probe with wraparound within the band, claimed against
|
|
46
|
+
* the shared `usedPriorities`. Throws only if the (40 000-slot) band is exhausted
|
|
47
|
+
* — unreachable in practice given AWS's per-listener rule ceiling.
|
|
48
|
+
*/
|
|
49
|
+
export function deterministicHostPriority(host, state) {
|
|
50
|
+
const base = fnv1a32(host) % HOST_PRIORITY_BAND_SIZE;
|
|
51
|
+
for (let probe = 0; probe < HOST_PRIORITY_BAND_SIZE; probe++) {
|
|
52
|
+
const priority = HOST_PRIORITY_BAND_START + ((base + probe) % HOST_PRIORITY_BAND_SIZE);
|
|
53
|
+
if (!state.usedPriorities.has(priority)) {
|
|
54
|
+
state.usedPriorities.add(priority);
|
|
55
|
+
return priority;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`Exhausted the host-priority band resolving a priority for "${host}"`);
|
|
59
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type IApplicationTargetGroup } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
2
|
+
import type { IFunction } from "aws-cdk-lib/aws-lambda";
|
|
3
|
+
import type { Construct } from "constructs";
|
|
4
|
+
export interface LambdaAlbTargetOptions {
|
|
5
|
+
readonly handler: IFunction;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Context-free Lambda-behind-ALB target group. Registering the handler as a
|
|
9
|
+
* `LambdaTarget` auto-adds the `elasticloadbalancing.amazonaws.com` invoke
|
|
10
|
+
* permission (CDK's `LambdaTarget.attachToApplicationTargetGroup` → `grantInvoke`),
|
|
11
|
+
* so no explicit `AWS::Lambda::Permission` is needed.
|
|
12
|
+
*
|
|
13
|
+
* Intentionally NOT attached to a listener here — the substrate exports the
|
|
14
|
+
* target-group ARN (A7) so slot-ops point a sleeping slot's host-header rule at
|
|
15
|
+
* it (§5.4); an unmatched host still falls through to the listener's 404.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createLambdaTargetGroup(scope: Construct, id: string, options: LambdaAlbTargetOptions): IApplicationTargetGroup;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ApplicationTargetGroup, TargetType } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
2
|
+
import { LambdaTarget } from "aws-cdk-lib/aws-elasticloadbalancingv2-targets";
|
|
3
|
+
/**
|
|
4
|
+
* Context-free Lambda-behind-ALB target group. Registering the handler as a
|
|
5
|
+
* `LambdaTarget` auto-adds the `elasticloadbalancing.amazonaws.com` invoke
|
|
6
|
+
* permission (CDK's `LambdaTarget.attachToApplicationTargetGroup` → `grantInvoke`),
|
|
7
|
+
* so no explicit `AWS::Lambda::Permission` is needed.
|
|
8
|
+
*
|
|
9
|
+
* Intentionally NOT attached to a listener here — the substrate exports the
|
|
10
|
+
* target-group ARN (A7) so slot-ops point a sleeping slot's host-header rule at
|
|
11
|
+
* it (§5.4); an unmatched host still falls through to the listener's 404.
|
|
12
|
+
*/
|
|
13
|
+
export function createLambdaTargetGroup(scope, id, options) {
|
|
14
|
+
return new ApplicationTargetGroup(scope, id, {
|
|
15
|
+
targetType: TargetType.LAMBDA,
|
|
16
|
+
targets: [new LambdaTarget(options.handler)]
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ApplicationListener, type ApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
2
|
+
import type { ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
3
|
+
export interface RoutingListenerOptions {
|
|
4
|
+
readonly port: number;
|
|
5
|
+
readonly certificate?: ICertificate;
|
|
6
|
+
/**
|
|
7
|
+
* Attach a fixed 404 default action. The ECS path passes this conditionally
|
|
8
|
+
* (only when ≥2 routes exist or no service has a port — CDK rejects a listener
|
|
9
|
+
* with neither a default action nor target groups); the dev substrate passes
|
|
10
|
+
* `true` unconditionally, since its host-header rules are the only routes and
|
|
11
|
+
* an unmatched host must return 404, not fall through.
|
|
12
|
+
*/
|
|
13
|
+
readonly default404: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Context-free ALB listener factory shared by the ECS cluster path and the dev
|
|
17
|
+
* substrate. When `default404` is set, the listener's default action is a plain
|
|
18
|
+
* `404 Not Found` fixed response; otherwise no default action is attached (the
|
|
19
|
+
* caller's target groups become the listener's default).
|
|
20
|
+
*/
|
|
21
|
+
export declare function addRoutingListener(loadBalancer: ApplicationLoadBalancer, id: string, options: RoutingListenerOptions): ApplicationListener;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ListenerAction } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
2
|
+
/**
|
|
3
|
+
* Context-free ALB listener factory shared by the ECS cluster path and the dev
|
|
4
|
+
* substrate. When `default404` is set, the listener's default action is a plain
|
|
5
|
+
* `404 Not Found` fixed response; otherwise no default action is attached (the
|
|
6
|
+
* caller's target groups become the listener's default).
|
|
7
|
+
*/
|
|
8
|
+
export function addRoutingListener(loadBalancer, id, options) {
|
|
9
|
+
const defaultAction = options.default404
|
|
10
|
+
? ListenerAction.fixedResponse(404, {
|
|
11
|
+
contentType: "text/plain",
|
|
12
|
+
messageBody: "Not Found"
|
|
13
|
+
})
|
|
14
|
+
: undefined;
|
|
15
|
+
return loadBalancer.addListener(id, {
|
|
16
|
+
port: options.port,
|
|
17
|
+
...(options.certificate && { certificates: [options.certificate] }),
|
|
18
|
+
...(defaultAction !== undefined && { defaultAction })
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -10,8 +10,24 @@ interface RdsProps {
|
|
|
10
10
|
vpc: IVpc;
|
|
11
11
|
databaseName?: string;
|
|
12
12
|
engine?: IClusterEngine;
|
|
13
|
+
/**
|
|
14
|
+
* Aurora PostgreSQL version string the `engine` was built at, carried purely
|
|
15
|
+
* so the auto-pause floor can be validated here (the layer that owns the
|
|
16
|
+
* min-capacity knob). `undefined` means the engine default (>= 16.3) is in use.
|
|
17
|
+
*/
|
|
18
|
+
engineVersion?: string;
|
|
13
19
|
engineConfig?: EngineConfig;
|
|
14
20
|
clusterIdentifier?: string;
|
|
21
|
+
/** Physical name of the generated master-credentials secret. */
|
|
22
|
+
credentialsSecretName?: string;
|
|
23
|
+
/** Serverless-v2 floor in ACU. 0 enables scale-to-zero (auto-pause). */
|
|
24
|
+
serverlessV2MinCapacity?: number;
|
|
25
|
+
/** Serverless-v2 ceiling in ACU. */
|
|
26
|
+
serverlessV2MaxCapacity?: number;
|
|
27
|
+
/** Idle duration before an idle serverless-v2 cluster auto-pauses to 0 ACU. */
|
|
28
|
+
serverlessV2AutoPauseDuration?: Duration;
|
|
29
|
+
/** Enables the RDS Data API (HTTP endpoint) on the cluster. */
|
|
30
|
+
enableDataApi?: boolean;
|
|
15
31
|
writer?: AuroraWriterConfig;
|
|
16
32
|
readers?: AuroraReadersConfig | false;
|
|
17
33
|
proxy?: ProxyConfig | false;
|
|
@@ -8,10 +8,49 @@ import { Secret } from "../secrets/index.js";
|
|
|
8
8
|
import { ResourceNaming } from "../../../utils/resourceNaming.js";
|
|
9
9
|
import { addProxyCfnOutput } from "./rdsProxyOutput.js";
|
|
10
10
|
import { getDatabaseInsightsRetention } from "./index.js";
|
|
11
|
-
import { RDS_DEFAULTS } from "./rdsDefaults.js";
|
|
11
|
+
import { RDS_DEFAULTS, SERVERLESS_V2_AUTO_PAUSE_MIN_SECONDS, SERVERLESS_V2_AUTO_PAUSE_MAX_SECONDS, MIN_AUTO_PAUSE_POSTGRES_VERSION } from "./rdsDefaults.js";
|
|
12
12
|
import { createRdsAlarms } from "../monitoring/index.js";
|
|
13
13
|
import { DEFAULT_POSTGRES_ENGINE_CONFIG, resolveDatabaseInsights, resolveStorageEncryptionKey, resolvePerformanceInsightsKey, addMultiUserSecretRotation, warnIfSnapshotUsernameAssumed } from "./rdsHelpers.js";
|
|
14
14
|
import { resolveSecretRotation } from "../../../utils/databaseTypes.js";
|
|
15
|
+
function parsePostgresVersion(version) {
|
|
16
|
+
const parts = version.split(".");
|
|
17
|
+
return {
|
|
18
|
+
major: parseInt(parts[0] ?? "", 10),
|
|
19
|
+
minor: parseInt(parts[1] ?? "0", 10)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function assertAutoPauseCapablePostgresVersion(engineVersion) {
|
|
23
|
+
const { major, minor } = parsePostgresVersion(engineVersion);
|
|
24
|
+
if (Number.isNaN(major) || Number.isNaN(minor)) {
|
|
25
|
+
throw new Error(`Invalid Aurora engineVersion "${engineVersion}" (expected "<major>.<minor>")`);
|
|
26
|
+
}
|
|
27
|
+
const floor = parsePostgresVersion(MIN_AUTO_PAUSE_POSTGRES_VERSION);
|
|
28
|
+
const meetsFloor = major > floor.major || (major === floor.major && minor >= floor.minor);
|
|
29
|
+
if (!meetsFloor) {
|
|
30
|
+
throw new Error(`serverlessV2MinCapacity 0 requires Aurora PostgreSQL >= ${MIN_AUTO_PAUSE_POSTGRES_VERSION} for auto-pause; received engineVersion "${engineVersion}"`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function validateServerlessV2DevKnobs(props) {
|
|
34
|
+
const min = props.serverlessV2MinCapacity;
|
|
35
|
+
const max = props.serverlessV2MaxCapacity;
|
|
36
|
+
if (min !== undefined && max !== undefined && min > max) {
|
|
37
|
+
throw new Error(`serverlessV2MinCapacity (${min}) must be <= serverlessV2MaxCapacity (${max})`);
|
|
38
|
+
}
|
|
39
|
+
if (props.serverlessV2AutoPauseDuration !== undefined) {
|
|
40
|
+
const seconds = props.serverlessV2AutoPauseDuration.toSeconds();
|
|
41
|
+
if (seconds < SERVERLESS_V2_AUTO_PAUSE_MIN_SECONDS ||
|
|
42
|
+
seconds > SERVERLESS_V2_AUTO_PAUSE_MAX_SECONDS) {
|
|
43
|
+
throw new Error(`serverlessV2AutoPauseDuration must be between ${SERVERLESS_V2_AUTO_PAUSE_MIN_SECONDS}s and ${SERVERLESS_V2_AUTO_PAUSE_MAX_SECONDS}s; received ${seconds}s`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Floor enforced only via engineVersion (opaque `IClusterEngine` can't be
|
|
47
|
+
// introspected): a raw `engine` below the floor with min 0 and no version is
|
|
48
|
+
// the caller's own risk. No such caller exists — DevSubstrate always passes
|
|
49
|
+
// engineVersion.
|
|
50
|
+
if (min === 0 && props.engineVersion !== undefined) {
|
|
51
|
+
assertAutoPauseCapablePostgresVersion(props.engineVersion);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
15
54
|
export class RdsAurora extends Construct {
|
|
16
55
|
connections;
|
|
17
56
|
constructId;
|
|
@@ -25,6 +64,7 @@ export class RdsAurora extends Construct {
|
|
|
25
64
|
databaseNameValue;
|
|
26
65
|
constructor(scope, id, props) {
|
|
27
66
|
super(scope, id);
|
|
67
|
+
validateServerlessV2DevKnobs(props);
|
|
28
68
|
this.constructId = id;
|
|
29
69
|
this.databaseNameValue = props.databaseName ?? id;
|
|
30
70
|
// PostgreSQL fallback for direct usage - ensure engine and engineConfig match
|
|
@@ -44,7 +84,8 @@ export class RdsAurora extends Construct {
|
|
|
44
84
|
// Global secondary clusters import replicated secret instead of creating new
|
|
45
85
|
if (props.isGlobalSecondary) {
|
|
46
86
|
this.databaseCredentials = new Secret(this, `${this.databaseNameValue}Credentials`, {
|
|
47
|
-
secretName:
|
|
87
|
+
secretName: props.credentialsSecretName ??
|
|
88
|
+
ResourceNaming.credentialsSecretName(id),
|
|
48
89
|
importExisting: true
|
|
49
90
|
});
|
|
50
91
|
}
|
|
@@ -57,7 +98,8 @@ export class RdsAurora extends Construct {
|
|
|
57
98
|
assumedUsername: username
|
|
58
99
|
});
|
|
59
100
|
this.databaseCredentials = new Secret(this, `${this.databaseNameValue}Credentials`, {
|
|
60
|
-
secretName:
|
|
101
|
+
secretName: props.credentialsSecretName ??
|
|
102
|
+
ResourceNaming.credentialsSecretName(id),
|
|
61
103
|
generateSecretString: {
|
|
62
104
|
secretStringTemplate: JSON.stringify({ username }),
|
|
63
105
|
excludePunctuation: true,
|
|
@@ -140,6 +182,18 @@ export class RdsAurora extends Construct {
|
|
|
140
182
|
removalPolicy: RemovalPolicy.SNAPSHOT,
|
|
141
183
|
deletionProtection: props.deletionProtection ?? true,
|
|
142
184
|
iamAuthentication: true,
|
|
185
|
+
...(props.serverlessV2MinCapacity !== undefined && {
|
|
186
|
+
serverlessV2MinCapacity: props.serverlessV2MinCapacity
|
|
187
|
+
}),
|
|
188
|
+
...(props.serverlessV2MaxCapacity !== undefined && {
|
|
189
|
+
serverlessV2MaxCapacity: props.serverlessV2MaxCapacity
|
|
190
|
+
}),
|
|
191
|
+
...(props.serverlessV2AutoPauseDuration !== undefined && {
|
|
192
|
+
serverlessV2AutoPauseDuration: props.serverlessV2AutoPauseDuration
|
|
193
|
+
}),
|
|
194
|
+
...(props.enableDataApi !== undefined && {
|
|
195
|
+
enableDataApi: props.enableDataApi
|
|
196
|
+
}),
|
|
143
197
|
writer,
|
|
144
198
|
readers
|
|
145
199
|
};
|
|
@@ -11,3 +11,31 @@ export declare const RDS_DEFAULTS: Readonly<{
|
|
|
11
11
|
/** Default storage autoscaling ceiling (GiB) — applied to primary and replica */
|
|
12
12
|
readonly DEFAULT_MAX_ALLOCATED_STORAGE_GIB: 500;
|
|
13
13
|
}>;
|
|
14
|
+
/**
|
|
15
|
+
* Serverless-v2 defaults for the internal dev substrate's shared Aurora cluster
|
|
16
|
+
* (fast dev-envs Phase 3). `min 0` ACU scales the shared cluster to zero when
|
|
17
|
+
* idle; `enableDataApi` lets slot DDL run over HTTPS with no VPC connection, and
|
|
18
|
+
* is the only mode under which min-0 actually saves (a live pooled connection
|
|
19
|
+
* pins the cluster above 0 ACU). Engine 16.6 satisfies the auto-pause-capable
|
|
20
|
+
* floor (see `MIN_AUTO_PAUSE_POSTGRES_VERSION`).
|
|
21
|
+
*/
|
|
22
|
+
export declare const DEV_AURORA_DEFAULTS: Readonly<{
|
|
23
|
+
readonly SERVERLESS_V2_MIN_CAPACITY: 0;
|
|
24
|
+
readonly SERVERLESS_V2_MAX_CAPACITY: 4;
|
|
25
|
+
readonly AUTO_PAUSE_SECONDS: 1200;
|
|
26
|
+
readonly ENABLE_DATA_API: true;
|
|
27
|
+
readonly ENGINE_VERSION: "16.6";
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Bounds AWS enforces on `ServerlessV2ScalingConfiguration.SecondsUntilAutoPause`
|
|
31
|
+
* (5 minutes – 24 hours). A value outside this range synths but is rejected at
|
|
32
|
+
* deploy, so the resources layer validates it at synth instead.
|
|
33
|
+
*/
|
|
34
|
+
export declare const SERVERLESS_V2_AUTO_PAUSE_MIN_SECONDS = 300;
|
|
35
|
+
export declare const SERVERLESS_V2_AUTO_PAUSE_MAX_SECONDS = 86400;
|
|
36
|
+
/**
|
|
37
|
+
* Minimum Aurora PostgreSQL version that supports serverless-v2 auto-pause
|
|
38
|
+
* (`serverlessV2MinCapacity` 0). An explicit override below this synths clean
|
|
39
|
+
* but AccessDenies / rejects at deploy, so the resources layer hard-guards it.
|
|
40
|
+
*/
|
|
41
|
+
export declare const MIN_AUTO_PAUSE_POSTGRES_VERSION = "16.3";
|
|
@@ -11,3 +11,31 @@ export const RDS_DEFAULTS = Object.freeze({
|
|
|
11
11
|
/** Default storage autoscaling ceiling (GiB) — applied to primary and replica */
|
|
12
12
|
DEFAULT_MAX_ALLOCATED_STORAGE_GIB: 500
|
|
13
13
|
});
|
|
14
|
+
/**
|
|
15
|
+
* Serverless-v2 defaults for the internal dev substrate's shared Aurora cluster
|
|
16
|
+
* (fast dev-envs Phase 3). `min 0` ACU scales the shared cluster to zero when
|
|
17
|
+
* idle; `enableDataApi` lets slot DDL run over HTTPS with no VPC connection, and
|
|
18
|
+
* is the only mode under which min-0 actually saves (a live pooled connection
|
|
19
|
+
* pins the cluster above 0 ACU). Engine 16.6 satisfies the auto-pause-capable
|
|
20
|
+
* floor (see `MIN_AUTO_PAUSE_POSTGRES_VERSION`).
|
|
21
|
+
*/
|
|
22
|
+
export const DEV_AURORA_DEFAULTS = Object.freeze({
|
|
23
|
+
SERVERLESS_V2_MIN_CAPACITY: 0,
|
|
24
|
+
SERVERLESS_V2_MAX_CAPACITY: 4,
|
|
25
|
+
AUTO_PAUSE_SECONDS: 1200,
|
|
26
|
+
ENABLE_DATA_API: true,
|
|
27
|
+
ENGINE_VERSION: "16.6"
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Bounds AWS enforces on `ServerlessV2ScalingConfiguration.SecondsUntilAutoPause`
|
|
31
|
+
* (5 minutes – 24 hours). A value outside this range synths but is rejected at
|
|
32
|
+
* deploy, so the resources layer validates it at synth instead.
|
|
33
|
+
*/
|
|
34
|
+
export const SERVERLESS_V2_AUTO_PAUSE_MIN_SECONDS = 300;
|
|
35
|
+
export const SERVERLESS_V2_AUTO_PAUSE_MAX_SECONDS = 86400;
|
|
36
|
+
/**
|
|
37
|
+
* Minimum Aurora PostgreSQL version that supports serverless-v2 auto-pause
|
|
38
|
+
* (`serverlessV2MinCapacity` 0). An explicit override below this synths clean
|
|
39
|
+
* but AccessDenies / rejects at deploy, so the resources layer hard-guards it.
|
|
40
|
+
*/
|
|
41
|
+
export const MIN_AUTO_PAUSE_POSTGRES_VERSION = "16.3";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tags applied by the internal `DevSubstrate` construct.
|
|
3
|
+
*
|
|
4
|
+
* `fjall:dev = "true"` is IAM-load-bearing, not cosmetic. The shipped dev-tier
|
|
5
|
+
* deploy fence in `@fjall/generator` (`src/schemas/devRolePolicies.ts`) gates its
|
|
6
|
+
* slot-ops grants on `aws:ResourceTag/fjall:dev = "true"` and
|
|
7
|
+
* `aws:RequestTag/fjall:dev = "true"`. The key and value here MUST stay
|
|
8
|
+
* byte-identical to those IAM condition keys — any drift silently unfences the
|
|
9
|
+
* substrate (the runtime-name-must-match-construct coupling class).
|
|
10
|
+
*
|
|
11
|
+
* A util-level single source shared with the policy is a candidate follow-up; it
|
|
12
|
+
* is deliberately NOT folded here, to avoid editing the parity-locked security
|
|
13
|
+
* policy from a construct-scaffold change.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEV_TAG_KEY: "fjall:dev";
|
|
16
|
+
export declare const DEV_TAG_VALUE: "true";
|
|
17
|
+
/**
|
|
18
|
+
* Reaper / organisational-mapping tag carrying the real customer application id
|
|
19
|
+
* (distinct from the `fjall-dev-<app>` CDK App identity that drives the
|
|
20
|
+
* substrate's stack names). NOT part of the IAM fence contract.
|
|
21
|
+
*/
|
|
22
|
+
export declare const DEV_APP_TAG_KEY: "fjall:app";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tags applied by the internal `DevSubstrate` construct.
|
|
3
|
+
*
|
|
4
|
+
* `fjall:dev = "true"` is IAM-load-bearing, not cosmetic. The shipped dev-tier
|
|
5
|
+
* deploy fence in `@fjall/generator` (`src/schemas/devRolePolicies.ts`) gates its
|
|
6
|
+
* slot-ops grants on `aws:ResourceTag/fjall:dev = "true"` and
|
|
7
|
+
* `aws:RequestTag/fjall:dev = "true"`. The key and value here MUST stay
|
|
8
|
+
* byte-identical to those IAM condition keys — any drift silently unfences the
|
|
9
|
+
* substrate (the runtime-name-must-match-construct coupling class).
|
|
10
|
+
*
|
|
11
|
+
* A util-level single source shared with the policy is a candidate follow-up; it
|
|
12
|
+
* is deliberately NOT folded here, to avoid editing the parity-locked security
|
|
13
|
+
* policy from a construct-scaffold change.
|
|
14
|
+
*/
|
|
15
|
+
export const DEV_TAG_KEY = "fjall:dev";
|
|
16
|
+
export const DEV_TAG_VALUE = "true";
|
|
17
|
+
/**
|
|
18
|
+
* Reaper / organisational-mapping tag carrying the real customer application id
|
|
19
|
+
* (distinct from the `fjall-dev-<app>` CDK App identity that drives the
|
|
20
|
+
* substrate's stack names). NOT part of the IAM fence contract.
|
|
21
|
+
*/
|
|
22
|
+
export const DEV_APP_TAG_KEY = "fjall:app";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.32.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"clean": "rm -rf ./dist",
|
|
35
35
|
"clean:node": "rm -rf ./node_modules",
|
|
36
36
|
"build:cert-gen-lambda": "node lib/lambda-assets/cert-generator/src/build.mjs",
|
|
37
|
-
"build": "npm run build:cert-gen-lambda && tsc && cp -r lib/layers dist/lib/layers && mkdir -p dist/lib/lambda-assets/cert-generator/asset && cp lib/lambda-assets/cert-generator/asset/index.js dist/lib/lambda-assets/cert-generator/asset/index.js && cp lib/lambda-assets/cert-generator/asset/package.json dist/lib/lambda-assets/cert-generator/asset/package.json && mkdir -p dist/lib/lambda-assets/identity-store-user/asset && cp lib/lambda-assets/identity-store-user/asset/index.js dist/lib/lambda-assets/identity-store-user/asset/index.js && cp lib/lambda-assets/identity-store-user/asset/package.json dist/lib/lambda-assets/identity-store-user/asset/package.json && mkdir -p dist/lib/lambda-assets/static-site-forms/asset && cp lib/lambda-assets/static-site-forms/asset/index.js dist/lib/lambda-assets/static-site-forms/asset/index.js && cp lib/lambda-assets/static-site-forms/asset/package.json dist/lib/lambda-assets/static-site-forms/asset/package.json && cp lib/resources/aws/compute/lifecycleHookLambda.source.cjs dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs && cp lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs && cp lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs",
|
|
37
|
+
"build": "npm run build:cert-gen-lambda && tsc && cp -r lib/layers dist/lib/layers && mkdir -p dist/lib/lambda-assets/cert-generator/asset && cp lib/lambda-assets/cert-generator/asset/index.js dist/lib/lambda-assets/cert-generator/asset/index.js && cp lib/lambda-assets/cert-generator/asset/package.json dist/lib/lambda-assets/cert-generator/asset/package.json && mkdir -p dist/lib/lambda-assets/identity-store-user/asset && cp lib/lambda-assets/identity-store-user/asset/index.js dist/lib/lambda-assets/identity-store-user/asset/index.js && cp lib/lambda-assets/identity-store-user/asset/package.json dist/lib/lambda-assets/identity-store-user/asset/package.json && mkdir -p dist/lib/lambda-assets/static-site-forms/asset && cp lib/lambda-assets/static-site-forms/asset/index.js dist/lib/lambda-assets/static-site-forms/asset/index.js && cp lib/lambda-assets/static-site-forms/asset/package.json dist/lib/lambda-assets/static-site-forms/asset/package.json && cp lib/resources/aws/compute/lifecycleHookLambda.source.cjs dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs && cp lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs && cp lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs && cp lib/patterns/aws/devSubstrate.waker.source.cjs dist/lib/patterns/aws/devSubstrate.waker.source.cjs",
|
|
38
38
|
"prepack": "node ../../scripts/check-dist-freshness.mjs",
|
|
39
39
|
"watch": "tsc -w",
|
|
40
40
|
"watch:only": "tsc -w",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
70
|
-
"@fjall/generator": "^2.
|
|
71
|
-
"@fjall/util": "^2.
|
|
70
|
+
"@fjall/generator": "^2.32.0",
|
|
71
|
+
"@fjall/util": "^2.32.0",
|
|
72
72
|
"constructs": "^10.6.0"
|
|
73
73
|
},
|
|
74
74
|
"overrides": {
|
|
@@ -82,5 +82,5 @@
|
|
|
82
82
|
"engines": {
|
|
83
83
|
"node": ">=18.0.0"
|
|
84
84
|
},
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "876a79a9ad0031a5919019c86867c28c1ab9dc92"
|
|
86
86
|
}
|