@fjall/components-infrastructure 2.30.3 → 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/config/aws/identityCenterMembership.d.ts +11 -0
- package/dist/lib/config/aws/identityCenterMembership.js +61 -0
- package/dist/lib/config/aws/oidcConnector.js +11 -3
- package/dist/lib/lambda-assets/cert-generator/asset/index.js +12 -3
- package/dist/lib/patterns/aws/compute.d.ts +2 -2
- package/dist/lib/patterns/aws/compute.js +18 -9
- package/dist/lib/patterns/aws/computeLambda.d.ts +15 -1
- package/dist/lib/patterns/aws/computeLambda.js +18 -2
- package/dist/lib/patterns/aws/computePropApplicability.d.ts +20 -0
- package/dist/lib/patterns/aws/computePropApplicability.js +87 -0
- 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/ecsImages.js +7 -2
- 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/imageTagParameter.d.ts +18 -1
- package/dist/lib/resources/aws/compute/imageTagParameter.js +32 -2
- package/dist/lib/resources/aws/compute/lambda.d.ts +1 -1
- package/dist/lib/resources/aws/compute/lambda.js +6 -4
- 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/resources/aws/iam/identityCenter/user.js +5 -1
- package/dist/lib/resources/aws/logging/index.d.ts +1 -0
- package/dist/lib/resources/aws/logging/index.js +1 -0
- package/dist/lib/resources/aws/logging/logGroup.d.ts +7 -0
- package/dist/lib/resources/aws/logging/logGroup.js +7 -0
- package/dist/lib/resources/aws/logging/singletonLogGroup.d.ts +15 -0
- package/dist/lib/resources/aws/logging/singletonLogGroup.js +24 -0
- package/dist/lib/resources/aws/networking/vpcPeeringConnection.js +2 -1
- package/dist/lib/resources/aws/storage/bucketDeployment.d.ts +5 -6
- package/dist/lib/resources/aws/storage/bucketDeployment.js +14 -7
- package/dist/lib/resources/aws/utilities/awsCustomResource.js +5 -0
- package/dist/lib/resources/aws/utilities/customResource.js +5 -1
- package/dist/lib/utils/addSuffixToEmail.js +1 -5
- package/dist/lib/utils/devSubstrateTags.d.ts +22 -0
- package/dist/lib/utils/devSubstrateTags.js +22 -0
- package/dist/lib/utils/validationLogger.d.ts +2 -2
- package/dist/lib/utils/validationLogger.js +4 -4
- package/package.json +5 -5
- package/dist/lib/__tests__/setup.d.ts +0 -48
- package/dist/lib/__tests__/setup.js +0 -1
- package/dist/lib/config/aws/accountId.d.ts +0 -6
- package/dist/lib/config/aws/accountId.js +0 -33
- package/dist/lib/config/aws/backupGlobalSettings.d.ts +0 -29
- package/dist/lib/config/aws/backupGlobalSettings.js +0 -51
- package/dist/lib/config/aws/costAllocationTags.d.ts +0 -12
- package/dist/lib/config/aws/costAllocationTags.js +0 -48
- package/dist/lib/config/aws/eventBus.d.ts +0 -7
- package/dist/lib/config/aws/eventBus.js +0 -24
- package/dist/lib/config/aws/identityCenterGroupMembership.d.ts +0 -10
- package/dist/lib/config/aws/identityCenterGroupMembership.js +0 -115
- package/dist/lib/config/aws/ipamDelegateAdmin.d.ts +0 -8
- package/dist/lib/config/aws/ipamDelegateAdmin.js +0 -58
- package/dist/lib/config/aws/ipamPoolId.d.ts +0 -10
- package/dist/lib/config/aws/ipamPoolId.js +0 -39
- package/dist/lib/config/aws/organisation.d.ts +0 -30
- package/dist/lib/config/aws/organisation.js +0 -95
- package/dist/lib/config/aws/organisationId.d.ts +0 -7
- package/dist/lib/config/aws/organisationId.js +0 -46
- package/dist/lib/config/aws/organisationsAccess.d.ts +0 -10
- package/dist/lib/config/aws/organisationsAccess.js +0 -50
- package/dist/lib/config/aws/ramSharing.d.ts +0 -4
- package/dist/lib/config/aws/ramSharing.js +0 -35
- package/dist/lib/config/monitoring.d.ts +0 -18
- package/dist/lib/config/monitoring.js +0 -22
- package/dist/lib/patterns/aws/cicdRole.d.ts +0 -67
- package/dist/lib/patterns/aws/cicdRole.js +0 -68
- package/dist/lib/patterns/aws/hostedZone.d.ts +0 -28
- package/dist/lib/patterns/aws/hostedZone.js +0 -150
- package/dist/lib/patterns/aws/managedAccount.d.ts +0 -9
- package/dist/lib/patterns/aws/managedAccount.js +0 -55
- package/dist/lib/patterns/aws/managedIdentityCenter.d.ts +0 -4
- package/dist/lib/patterns/aws/managedIdentityCenter.js +0 -24
- package/dist/lib/patterns/aws/managedOrganisation.d.ts +0 -36
- package/dist/lib/patterns/aws/managedOrganisation.js +0 -97
- package/dist/lib/patterns/aws/managedPlatform.d.ts +0 -12
- package/dist/lib/patterns/aws/managedPlatform.js +0 -29
- package/dist/lib/patterns/aws/subdomainHostedZone.d.ts +0 -9
- package/dist/lib/patterns/aws/subdomainHostedZone.js +0 -39
- package/dist/lib/resources/aws/cicd/cicdRole.d.ts +0 -65
- package/dist/lib/resources/aws/cicd/cicdRole.js +0 -191
- package/dist/lib/resources/aws/compute/ecsFreeTier.d.ts +0 -75
- package/dist/lib/resources/aws/compute/ecsFreeTier.js +0 -1
- package/dist/lib/resources/aws/compute/ecsSpot.d.ts +0 -75
- package/dist/lib/resources/aws/compute/ecsSpot.js +0 -1
- package/dist/lib/resources/aws/compute/utilities/capacityProviderDrainWaiter.d.ts +0 -20
- package/dist/lib/resources/aws/compute/utilities/capacityProviderDrainWaiter.js +0 -1
- package/dist/lib/resources/aws/database/database.d.ts +0 -14
- package/dist/lib/resources/aws/database/database.js +0 -28
- package/dist/lib/resources/aws/database/databaseInstance.d.ts +0 -15
- package/dist/lib/resources/aws/database/databaseInstance.js +0 -30
- package/dist/lib/resources/aws/iam/identityCenter/attachManagedPolicy.d.ts +0 -13
- package/dist/lib/resources/aws/iam/identityCenter/attachManagedPolicy.js +0 -52
- package/dist/lib/resources/aws/iam/securityGroup.d.ts +0 -5
- package/dist/lib/resources/aws/iam/securityGroup.js +0 -14
- package/dist/lib/resources/aws/monitoring/monitoringRole.d.ts +0 -28
- package/dist/lib/resources/aws/monitoring/monitoringRole.js +0 -125
- package/dist/lib/resources/aws/utilities/cfnOutput.d.ts +0 -5
- package/dist/lib/resources/aws/utilities/cfnOutput.js +0 -1
- package/dist/lib/utils/capitalizeString.d.ts +0 -1
- package/dist/lib/utils/capitalizeString.js +0 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Port
|
|
1
|
+
import { ApplicationProtocol, ListenerAction } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
2
|
+
import { Port } from "aws-cdk-lib/aws-ec2";
|
|
3
3
|
import { CfnOutput, Duration, Fn } from "aws-cdk-lib";
|
|
4
4
|
import { Certificate, CertificateValidation } from "aws-cdk-lib/aws-certificatemanager";
|
|
5
5
|
import { ARecord, HostedZone as AWSHostedZone, RecordTarget } from "aws-cdk-lib/aws-route53";
|
|
@@ -7,25 +7,9 @@ import { LoadBalancerTarget } from "aws-cdk-lib/aws-route53-targets";
|
|
|
7
7
|
import { HostedZone as FjallHostedZone } from "../networking/hostedZone.js";
|
|
8
8
|
import { SecurityGroup } from "../networking/securityGroup.js";
|
|
9
9
|
import { isServiceEc2 } from "./ecsTaskDefinition.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
state.nextPriority++;
|
|
14
|
-
}
|
|
15
|
-
const priority = state.nextPriority++;
|
|
16
|
-
state.usedPriorities.add(priority);
|
|
17
|
-
return priority;
|
|
18
|
-
}
|
|
19
|
-
export function buildRoutingConditions(rule) {
|
|
20
|
-
const conditions = [];
|
|
21
|
-
if (rule?.path) {
|
|
22
|
-
conditions.push(ListenerCondition.pathPatterns([rule.path]));
|
|
23
|
-
}
|
|
24
|
-
if (rule?.host) {
|
|
25
|
-
conditions.push(ListenerCondition.hostHeaders([rule.host]));
|
|
26
|
-
}
|
|
27
|
-
return conditions;
|
|
28
|
-
}
|
|
10
|
+
import { createApplicationLoadBalancer } from "./applicationLoadBalancer.js";
|
|
11
|
+
import { addRoutingListener } from "./listenerRouting.js";
|
|
12
|
+
import { buildRoutingConditions, getNextPriority } from "./hostHeaderListenerRule.js";
|
|
29
13
|
export function addLoadBalancer(ctx, anyServiceUsesEc2, asgSecurityGroup) {
|
|
30
14
|
const props = ctx.props;
|
|
31
15
|
const defaultLoadBalancerName = `${props.clusterName}LoadBalancer`;
|
|
@@ -48,18 +32,13 @@ export function addLoadBalancer(ctx, anyServiceUsesEc2, asgSecurityGroup) {
|
|
|
48
32
|
asgSecurityGroup.connections.allowFrom(loadBalancerSecurityGroup, Port.tcpRange(49152, 65535));
|
|
49
33
|
}
|
|
50
34
|
}
|
|
51
|
-
const loadBalancer =
|
|
35
|
+
const loadBalancer = createApplicationLoadBalancer(ctx.scope, `${props.clusterName}LoadBalancer`, {
|
|
52
36
|
vpc: ctx.cluster.vpc,
|
|
53
37
|
internetFacing: !isInternal,
|
|
38
|
+
loadBalancerName: truncatedLoadBalancerName,
|
|
54
39
|
...(loadBalancerSecurityGroup && {
|
|
55
40
|
securityGroup: loadBalancerSecurityGroup
|
|
56
|
-
})
|
|
57
|
-
loadBalancerName: truncatedLoadBalancerName,
|
|
58
|
-
vpcSubnets: {
|
|
59
|
-
subnetType: isInternal
|
|
60
|
-
? SubnetType.PRIVATE_WITH_EGRESS
|
|
61
|
-
: SubnetType.PUBLIC
|
|
62
|
-
}
|
|
41
|
+
})
|
|
63
42
|
});
|
|
64
43
|
new CfnOutput(ctx.scope, `${ctx.outputName}LoadBalancerDnsName`, {
|
|
65
44
|
key: `${ctx.outputName}LoadBalancerDnsName`,
|
|
@@ -112,25 +91,11 @@ export function addLoadBalancerListener(ctx, loadBalancer, certificate) {
|
|
|
112
91
|
});
|
|
113
92
|
// CDK rejects listeners with neither a default action nor target groups.
|
|
114
93
|
const noServicePorts = servicesWithPorts.length === 0;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
: undefined;
|
|
121
|
-
if (certificate) {
|
|
122
|
-
return loadBalancer.addListener(`${ctx.props.clusterName}Listener`, {
|
|
123
|
-
port,
|
|
124
|
-
certificates: [certificate],
|
|
125
|
-
...(defaultAction !== undefined && { defaultAction })
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
return loadBalancer.addListener(`${ctx.props.clusterName}Listener`, {
|
|
130
|
-
port,
|
|
131
|
-
...(defaultAction !== undefined && { defaultAction })
|
|
132
|
-
});
|
|
133
|
-
}
|
|
94
|
+
return addRoutingListener(loadBalancer, `${ctx.props.clusterName}Listener`, {
|
|
95
|
+
port,
|
|
96
|
+
...(certificate && { certificate }),
|
|
97
|
+
default404: willHaveMultipleRoutes || noServicePorts
|
|
98
|
+
});
|
|
134
99
|
}
|
|
135
100
|
export function addHostedZone(ctx, loadBalancer) {
|
|
136
101
|
const props = ctx.props;
|
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
import { Role } from "aws-cdk-lib/aws-iam";
|
|
2
|
+
import type { Construct } from "constructs";
|
|
2
3
|
import type { EcsConstructContext } from "./ecsContext.js";
|
|
3
4
|
import type { EcsServiceProps } from "./ecsTypes.js";
|
|
5
|
+
/**
|
|
6
|
+
* Builds the base ECS task-execution role shared by every task the platform
|
|
7
|
+
* runs: the ECR-pull quartet (account-level, so `resources: ["*"]`) and a
|
|
8
|
+
* ViaService-conditioned `kms:Decrypt` for SSM SecureString / Secrets-Manager
|
|
9
|
+
* CMK env injection. Pass `ssmSecretsPath` (already derived — no partition/
|
|
10
|
+
* region/account prefix) to add the path-scoped `ssm:GetParameters` grant.
|
|
11
|
+
* Trust principal is `ecs-tasks.amazonaws.com`.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately omits `logs:*` and `secretsmanager:GetSecretValue`: on the
|
|
14
|
+
* ECS-pattern path `AwsLogDriver.bind()` and `addContainer({ secrets })`
|
|
15
|
+
* auto-grant those on the exact LogGroup / secret ARNs. A caller that builds a
|
|
16
|
+
* SHARED or IMPORTED role those auto-grants cannot reach — e.g. the dev
|
|
17
|
+
* substrate's slot exec role, referenced by slot task-defs via ARN — MUST add
|
|
18
|
+
* the explicit `logs` / `secretsmanager` statements itself.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createBaseExecutionRole(scope: Construct, id: string, opts?: {
|
|
21
|
+
ssmSecretsPath?: string;
|
|
22
|
+
}): Role;
|
|
4
23
|
/**
|
|
5
24
|
* Creates the execution role for ECS infrastructure operations.
|
|
6
25
|
* Used by the ECS agent to pull images, write logs, and inject secrets.
|
|
7
26
|
* NOT used by application code — that's the task role.
|
|
27
|
+
*
|
|
28
|
+
* Delegates the ECR/kms/ssm shape to `createBaseExecutionRole`. Logs +
|
|
29
|
+
* secretsmanager are auto-granted downstream on this path (AwsLogDriver.bind()
|
|
30
|
+
* on the real LogGroup, addContainer({ secrets }) on the exact secret ARN), so a
|
|
31
|
+
* hand-rolled `/ecs/{cluster}*` logs or bare `-*` secrets statement here would be
|
|
32
|
+
* dead (the 2026-06-04 outage shape).
|
|
8
33
|
*/
|
|
9
34
|
export declare function createExecutionRole(ctx: EcsConstructContext, serviceName: string): Role;
|
|
10
35
|
/**
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { CfnParameter } from "aws-cdk-lib";
|
|
2
2
|
import { type Construct } from "constructs";
|
|
3
|
+
import { type DockerBuild } from "@fjall/util/manifest/schemas";
|
|
4
|
+
/** Which key namespace is registering the parameter. */
|
|
5
|
+
export type ImageTagEntityKind = "ecs-service" | "lambda-image";
|
|
6
|
+
/** Identity of the entity requesting the parameter, for collision reporting. */
|
|
7
|
+
export interface ImageTagEntity {
|
|
8
|
+
readonly kind: ImageTagEntityKind;
|
|
9
|
+
/** ECS: the service name. Lambda: the function's construct id. */
|
|
10
|
+
readonly declaredBy: string;
|
|
11
|
+
readonly docker: DockerBuild | undefined;
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
14
|
* Get or create the per-key image-tag CfnParameter (logical id from
|
|
5
15
|
* `imageTagParameterName`). Idempotent by logical id: a second call with the
|
|
@@ -8,5 +18,12 @@ import { type Construct } from "constructs";
|
|
|
8
18
|
* than once within a stack). Distinct keys that happen to collide on the same
|
|
9
19
|
* PascalCase logical id fail closed rather than silently aliasing two
|
|
10
20
|
* different things onto one parameter.
|
|
21
|
+
*
|
|
22
|
+
* Cross-entity reuse (an ECS service name and a Lambda `image` key sharing
|
|
23
|
+
* one build key) is allowed only when both declare fingerprint-identical
|
|
24
|
+
* docker configs — the deliberate shared-build case. Any divergence
|
|
25
|
+
* (including docker-vs-no-docker) throws at synth, mirroring deploy-core's
|
|
26
|
+
* `assertNoCrossEntityBuildKeyDivergence`: a shared parameter with divergent
|
|
27
|
+
* builds means a deploy of either entity silently rolls both onto one image.
|
|
11
28
|
*/
|
|
12
|
-
export declare function getOrCreateImageTagParameter(scope: Construct, key: string): CfnParameter;
|
|
29
|
+
export declare function getOrCreateImageTagParameter(scope: Construct, key: string, entity: ImageTagEntity): CfnParameter;
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { CfnParameter, Stack } from "aws-cdk-lib";
|
|
2
2
|
import { imageTagParameterName } from "@fjall/util";
|
|
3
|
+
import { dockerBuildFingerprint } from "@fjall/util/manifest/schemas";
|
|
4
|
+
/** Per-parameter entity metadata; WeakMap keeps the registry per-stack and test-safe. */
|
|
5
|
+
const registrations = new WeakMap();
|
|
3
6
|
function buildImageTagDescription(key) {
|
|
4
7
|
return `Image tag for ${key}. Set by fjall deploy to the content-hash tag.`;
|
|
5
8
|
}
|
|
9
|
+
function describeEntity(kind, declaredBy) {
|
|
10
|
+
return kind === "ecs-service"
|
|
11
|
+
? `ECS service "${declaredBy}"`
|
|
12
|
+
: `Lambda function "${declaredBy}"`;
|
|
13
|
+
}
|
|
6
14
|
/**
|
|
7
15
|
* Get or create the per-key image-tag CfnParameter (logical id from
|
|
8
16
|
* `imageTagParameterName`). Idempotent by logical id: a second call with the
|
|
@@ -11,8 +19,15 @@ function buildImageTagDescription(key) {
|
|
|
11
19
|
* than once within a stack). Distinct keys that happen to collide on the same
|
|
12
20
|
* PascalCase logical id fail closed rather than silently aliasing two
|
|
13
21
|
* different things onto one parameter.
|
|
22
|
+
*
|
|
23
|
+
* Cross-entity reuse (an ECS service name and a Lambda `image` key sharing
|
|
24
|
+
* one build key) is allowed only when both declare fingerprint-identical
|
|
25
|
+
* docker configs — the deliberate shared-build case. Any divergence
|
|
26
|
+
* (including docker-vs-no-docker) throws at synth, mirroring deploy-core's
|
|
27
|
+
* `assertNoCrossEntityBuildKeyDivergence`: a shared parameter with divergent
|
|
28
|
+
* builds means a deploy of either entity silently rolls both onto one image.
|
|
14
29
|
*/
|
|
15
|
-
export function getOrCreateImageTagParameter(scope, key) {
|
|
30
|
+
export function getOrCreateImageTagParameter(scope, key, entity) {
|
|
16
31
|
const stack = Stack.of(scope);
|
|
17
32
|
const paramLogicalId = imageTagParameterName(key);
|
|
18
33
|
const description = buildImageTagDescription(key);
|
|
@@ -22,11 +37,26 @@ export function getOrCreateImageTagParameter(scope, key) {
|
|
|
22
37
|
throw new Error(`CfnParameter logical-ID collision: "${paramLogicalId}" is already registered for a different service. ` +
|
|
23
38
|
`Distinct service/lambda names cannot share a PascalCase identifier — rename one of the conflicting entries.`);
|
|
24
39
|
}
|
|
40
|
+
const prior = registrations.get(existing);
|
|
41
|
+
const fingerprint = dockerBuildFingerprint(entity.docker);
|
|
42
|
+
if (prior !== undefined && prior.fingerprint !== fingerprint) {
|
|
43
|
+
throw new Error(`Image-tag parameter collision: ${describeEntity(prior.kind, prior.declaredBy)} and ` +
|
|
44
|
+
`${describeEntity(entity.kind, entity.declaredBy)} share the build key "${key}" ` +
|
|
45
|
+
`(CfnParameter "${paramLogicalId}") but declare different docker configs — ` +
|
|
46
|
+
`a deploy of either would silently roll both onto one image. Rename one of them, ` +
|
|
47
|
+
`or make the docker configs identical to deliberately share the build.`);
|
|
48
|
+
}
|
|
25
49
|
return existing;
|
|
26
50
|
}
|
|
27
|
-
|
|
51
|
+
const param = new CfnParameter(stack, paramLogicalId, {
|
|
28
52
|
type: "String",
|
|
29
53
|
default: "latest",
|
|
30
54
|
description
|
|
31
55
|
});
|
|
56
|
+
registrations.set(param, {
|
|
57
|
+
kind: entity.kind,
|
|
58
|
+
declaredBy: entity.declaredBy,
|
|
59
|
+
fingerprint: dockerBuildFingerprint(entity.docker)
|
|
60
|
+
});
|
|
61
|
+
return param;
|
|
32
62
|
}
|
|
@@ -2,7 +2,7 @@ import { SingletonFunction as singletonFunction, Function, Code, type Runtime, A
|
|
|
2
2
|
import { type Bucket } from "aws-cdk-lib/aws-s3";
|
|
3
3
|
import { PolicyStatement, type IRole } from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import { type IVpc } from "aws-cdk-lib/aws-ec2";
|
|
5
|
-
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
5
|
+
import type { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
6
6
|
import { type IQueue } from "aws-cdk-lib/aws-sqs";
|
|
7
7
|
import { type ITable } from "aws-cdk-lib/aws-dynamodb";
|
|
8
8
|
import { type Construct } from "constructs";
|
|
@@ -7,8 +7,7 @@ import { createHash } from "node:crypto";
|
|
|
7
7
|
import { SqsEventSource, DynamoEventSource, S3EventSource } from "aws-cdk-lib/aws-lambda-event-sources";
|
|
8
8
|
import { EventType } from "aws-cdk-lib/aws-s3";
|
|
9
9
|
import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
|
|
10
|
-
import {
|
|
11
|
-
import { LogGroup } from "../logging/logGroup.js";
|
|
10
|
+
import { DEFAULT_FRAMEWORK_LOG_RETENTION, LogGroup } from "../logging/logGroup.js";
|
|
12
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
12
|
import { buildParameterPath } from "@fjall/util";
|
|
14
13
|
import { resolveImportedSecret } from "../secrets/index.js";
|
|
@@ -95,7 +94,10 @@ export class SingletonFunction extends singletonFunction {
|
|
|
95
94
|
runtime: props.runtime,
|
|
96
95
|
ephemeralStorageSize: props.ephemeralStorageSize
|
|
97
96
|
? Size.mebibytes(props.ephemeralStorageSize)
|
|
98
|
-
: undefined
|
|
97
|
+
: undefined,
|
|
98
|
+
logGroup: new LogGroup(scope, `${id}LogGroup`, {
|
|
99
|
+
retention: props.logGroupRetention ?? DEFAULT_FRAMEWORK_LOG_RETENTION
|
|
100
|
+
})
|
|
99
101
|
});
|
|
100
102
|
addPoliciesToRole(this, props.inlinePolicy);
|
|
101
103
|
applyRoleDescription(this, props.roleDescription);
|
|
@@ -125,7 +127,7 @@ export class LambdaFunction extends Function {
|
|
|
125
127
|
description: props.lambdaDescription ?? `${id} Lambda`,
|
|
126
128
|
environment: props.environment,
|
|
127
129
|
logGroup: new LogGroup(scope, `${id}LogGroup`, {
|
|
128
|
-
retention: props.logGroupRetention ??
|
|
130
|
+
retention: props.logGroupRetention ?? DEFAULT_FRAMEWORK_LOG_RETENTION
|
|
129
131
|
})
|
|
130
132
|
});
|
|
131
133
|
addPoliciesToRole(this, props.inlinePolicy);
|
|
@@ -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
|
};
|