@friggframework/devtools 2.0.0--canary.629.1a7ad06.0 → 2.0.0--canary.640.b31eb4a.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/infrastructure/__tests__/aurora-scale-to-zero-connectivity.test.js +255 -0
- package/infrastructure/domains/database/aurora-builder.js +232 -39
- package/infrastructure/domains/networking/vpc-builder.js +65 -4
- package/infrastructure/domains/shared/types/app-definition.js +18 -2
- package/package.json +7 -7
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-033: Aurora Serverless v2 scale-to-zero + NAT-free public connectivity
|
|
3
|
+
*
|
|
4
|
+
* These tests assert on the GENERATED CloudFormation template (via the full
|
|
5
|
+
* composer) plus the AuroraBuilder validator. They are deterministic and require
|
|
6
|
+
* neither a live AWS account nor Prisma client generation:
|
|
7
|
+
* - Validator cases call AuroraBuilder.validate() directly (pure, no I/O).
|
|
8
|
+
* - Template cases use vpc.management='create-new' + database.postgres
|
|
9
|
+
* management='managed', which resolve to STACK ownership without depending on
|
|
10
|
+
* any discovered AWS resource, so cloud discovery returning empty is fine.
|
|
11
|
+
*
|
|
12
|
+
* Both new capabilities are opt-in and default-off; the final describe block
|
|
13
|
+
* guards the no-regression promise: a default (vpc, no minCapacity) definition
|
|
14
|
+
* composes the same template as before.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { composeServerlessDefinition } = require('../infrastructure-composer');
|
|
18
|
+
const { AuroraBuilder } = require('../domains/database/aurora-builder');
|
|
19
|
+
|
|
20
|
+
// Shared: an app definition that creates a fresh VPC + a Frigg-owned Aurora
|
|
21
|
+
// cluster in-stack, with overridable postgres config.
|
|
22
|
+
function makeApp(postgresOverrides = {}) {
|
|
23
|
+
return {
|
|
24
|
+
name: 'adr033-app',
|
|
25
|
+
provider: 'aws',
|
|
26
|
+
region: 'us-east-1',
|
|
27
|
+
integrations: [],
|
|
28
|
+
vpc: { enable: true, management: 'create-new' },
|
|
29
|
+
database: {
|
|
30
|
+
postgres: {
|
|
31
|
+
enable: true,
|
|
32
|
+
management: 'managed',
|
|
33
|
+
...postgresOverrides,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findResources(template, predicate) {
|
|
40
|
+
return Object.entries(template.resources.Resources).filter(([, r]) => predicate(r));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('ADR-033: Aurora scale-to-zero + connectivity', () => {
|
|
44
|
+
beforeAll(() => {
|
|
45
|
+
process.env.AWS_REGION = 'us-east-1';
|
|
46
|
+
// Intentionally NOT setting FRIGG_SKIP_AWS_DISCOVERY — the builders must
|
|
47
|
+
// execute. create-new/managed resolve to STACK without needing discovery.
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterAll(() => {
|
|
51
|
+
delete process.env.AWS_REGION;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------
|
|
55
|
+
// Validator
|
|
56
|
+
// ---------------------------------------------------------------------
|
|
57
|
+
describe('validator (AuroraBuilder.validate)', () => {
|
|
58
|
+
const build = new AuroraBuilder();
|
|
59
|
+
// ValidationResult exposes hasErrors(); "valid" means no errors.
|
|
60
|
+
const validateResult = (postgres) =>
|
|
61
|
+
build.validate({ database: { postgres: { enable: true, ...postgres } } });
|
|
62
|
+
const isValid = (postgres) => !validateResult(postgres).hasErrors();
|
|
63
|
+
|
|
64
|
+
test('accepts minCapacity: 0 (scale-to-zero)', () => {
|
|
65
|
+
expect(isValid({ minCapacity: 0 })).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('rejects minCapacity: 0.3 (inside the forbidden (0, 0.5) band)', () => {
|
|
69
|
+
const r = validateResult({ minCapacity: 0.3 });
|
|
70
|
+
expect(r.hasErrors()).toBe(true);
|
|
71
|
+
expect(r.errors.join(' ')).toMatch(/minCapacity must be 0 \(scale-to-zero\) or between 0\.5 and 128/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('accepts minCapacity: 0.5 and minCapacity: 64', () => {
|
|
75
|
+
expect(isValid({ minCapacity: 0.5 })).toBe(true);
|
|
76
|
+
expect(isValid({ minCapacity: 64 })).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('rejects secondsUntilAutoPause: 100 (below 300)', () => {
|
|
80
|
+
const r = validateResult({ minCapacity: 0, secondsUntilAutoPause: 100 });
|
|
81
|
+
expect(r.hasErrors()).toBe(true);
|
|
82
|
+
expect(r.errors.join(' ')).toMatch(/secondsUntilAutoPause must be an integer between 300 and 86400/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('accepts secondsUntilAutoPause: 3600', () => {
|
|
86
|
+
expect(isValid({ minCapacity: 0, secondsUntilAutoPause: 3600 })).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("accepts connectivity: 'public'", () => {
|
|
90
|
+
expect(isValid({ connectivity: 'public' })).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("rejects connectivity: 'nope'", () => {
|
|
94
|
+
const r = validateResult({ connectivity: 'nope' });
|
|
95
|
+
expect(r.hasErrors()).toBe(true);
|
|
96
|
+
expect(r.errors.join(' ')).toMatch(/Invalid database\.postgres\.connectivity/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('rejects non-array / non-CIDR allowedCidrs, accepts valid CIDRs', () => {
|
|
100
|
+
expect(isValid({ allowedCidrs: 'nope' })).toBe(false);
|
|
101
|
+
expect(isValid({ allowedCidrs: ['not-a-cidr'] })).toBe(false);
|
|
102
|
+
expect(isValid({ allowedCidrs: ['10.0.0.0/8', '203.0.113.5/32'] })).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('warns (does not fail) when minCapacity:0 with an older pinned engine version', () => {
|
|
106
|
+
const r = validateResult({ minCapacity: 0, engineVersion: '15.4' });
|
|
107
|
+
expect(r.hasErrors()).toBe(false); // warning, not error
|
|
108
|
+
expect(r.warnings.join(' ')).toMatch(/may not support .*scale-to-zero/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('does not warn about engine when minCapacity:0 on a capable version', () => {
|
|
112
|
+
const r = validateResult({ minCapacity: 0, engineVersion: '15.13' });
|
|
113
|
+
expect(r.warnings.join(' ')).not.toMatch(/may not support .*scale-to-zero/);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------
|
|
118
|
+
// Scale-to-zero (template shape)
|
|
119
|
+
// ---------------------------------------------------------------------
|
|
120
|
+
describe('scale-to-zero (minCapacity: 0)', () => {
|
|
121
|
+
test('MinCapacity is exactly 0 and SecondsUntilAutoPause defaults to 300', async () => {
|
|
122
|
+
const t = await composeServerlessDefinition(makeApp({ minCapacity: 0 }));
|
|
123
|
+
const scaling = t.resources.Resources.FriggAuroraCluster.Properties.ServerlessV2ScalingConfiguration;
|
|
124
|
+
|
|
125
|
+
// Mutation guard: with the old `|| 0.5` bug this would be 0.5, not 0.
|
|
126
|
+
expect(scaling.MinCapacity).toBe(0);
|
|
127
|
+
expect(scaling.MinCapacity).not.toBe(0.5);
|
|
128
|
+
expect(scaling.SecondsUntilAutoPause).toBe(300);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('SecondsUntilAutoPause honors a custom value', async () => {
|
|
132
|
+
const t = await composeServerlessDefinition(makeApp({ minCapacity: 0, secondsUntilAutoPause: 1800 }));
|
|
133
|
+
const scaling = t.resources.Resources.FriggAuroraCluster.Properties.ServerlessV2ScalingConfiguration;
|
|
134
|
+
expect(scaling.MinCapacity).toBe(0);
|
|
135
|
+
expect(scaling.SecondsUntilAutoPause).toBe(1800);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('MaxCapacity defaults to 4 and is preserved with scale-to-zero', async () => {
|
|
139
|
+
const t = await composeServerlessDefinition(makeApp({ minCapacity: 0 }));
|
|
140
|
+
const scaling = t.resources.Resources.FriggAuroraCluster.Properties.ServerlessV2ScalingConfiguration;
|
|
141
|
+
expect(scaling.MaxCapacity).toBe(4);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------
|
|
146
|
+
// Public connectivity (template shape)
|
|
147
|
+
// ---------------------------------------------------------------------
|
|
148
|
+
describe("connectivity: 'public'", () => {
|
|
149
|
+
test('Aurora ingress uses CidrIp — one rule per allowedCidr — not SourceSecurityGroupId', async () => {
|
|
150
|
+
const t = await composeServerlessDefinition(
|
|
151
|
+
makeApp({ connectivity: 'public', allowedCidrs: ['10.1.0.0/16', '203.0.113.7/32'] })
|
|
152
|
+
);
|
|
153
|
+
const ingress = findResources(
|
|
154
|
+
t,
|
|
155
|
+
(r) => r.Type === 'AWS::EC2::SecurityGroupIngress' && r.Properties.FromPort === 5432
|
|
156
|
+
);
|
|
157
|
+
expect(ingress).toHaveLength(2);
|
|
158
|
+
const cidrs = ingress.map(([, r]) => r.Properties.CidrIp).sort();
|
|
159
|
+
expect(cidrs).toEqual(['10.1.0.0/16', '203.0.113.7/32']);
|
|
160
|
+
ingress.forEach(([, r]) => {
|
|
161
|
+
expect(r.Properties.CidrIp).toBeDefined();
|
|
162
|
+
expect(r.Properties.SourceSecurityGroupId).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('allowedCidrs defaults to 0.0.0.0/0 when omitted', async () => {
|
|
167
|
+
const t = await composeServerlessDefinition(makeApp({ connectivity: 'public' }));
|
|
168
|
+
const ingress = findResources(
|
|
169
|
+
t,
|
|
170
|
+
(r) => r.Type === 'AWS::EC2::SecurityGroupIngress' && r.Properties.FromPort === 5432
|
|
171
|
+
);
|
|
172
|
+
expect(ingress).toHaveLength(1);
|
|
173
|
+
expect(ingress[0][1].Properties.CidrIp).toBe('0.0.0.0/0');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('Aurora instance is PubliclyAccessible and cluster sits in public subnets', async () => {
|
|
177
|
+
const t = await composeServerlessDefinition(makeApp({ connectivity: 'public' }));
|
|
178
|
+
expect(t.resources.Resources.FriggAuroraInstance.Properties.PubliclyAccessible).toBe(true);
|
|
179
|
+
expect(t.resources.Resources.FriggDBSubnetGroup.Properties.SubnetIds).toEqual([
|
|
180
|
+
{ Ref: 'FriggPublicSubnet' },
|
|
181
|
+
{ Ref: 'FriggPublicSubnet2' },
|
|
182
|
+
]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('NO NAT Gateway resource is emitted', async () => {
|
|
186
|
+
const t = await composeServerlessDefinition(makeApp({ connectivity: 'public' }));
|
|
187
|
+
const nats = findResources(t, (r) => r.Type === 'AWS::EC2::NatGateway');
|
|
188
|
+
expect(nats).toHaveLength(0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('Lambda is NOT attached to the VPC (provider.vpc unset)', async () => {
|
|
192
|
+
const t = await composeServerlessDefinition(makeApp({ connectivity: 'public' }));
|
|
193
|
+
expect(t.provider.vpc).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('DATABASE_URL enforces TLS (sslmode=require)', async () => {
|
|
197
|
+
const t = await composeServerlessDefinition(makeApp({ connectivity: 'public' }));
|
|
198
|
+
const url = t.provider.environment.DATABASE_URL;
|
|
199
|
+
expect(url['Fn::Sub'][0]).toContain('sslmode=require');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('combines with scale-to-zero: $0-idle NAT-free Aurora', async () => {
|
|
203
|
+
const t = await composeServerlessDefinition(makeApp({ connectivity: 'public', minCapacity: 0 }));
|
|
204
|
+
const scaling = t.resources.Resources.FriggAuroraCluster.Properties.ServerlessV2ScalingConfiguration;
|
|
205
|
+
expect(scaling.MinCapacity).toBe(0);
|
|
206
|
+
expect(scaling.SecondsUntilAutoPause).toBe(300);
|
|
207
|
+
expect(findResources(t, (r) => r.Type === 'AWS::EC2::NatGateway')).toHaveLength(0);
|
|
208
|
+
expect(t.provider.vpc).toBeUndefined();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------
|
|
213
|
+
// No-regression: default (vpc) connectivity, no new fields
|
|
214
|
+
// ---------------------------------------------------------------------
|
|
215
|
+
describe("default connectivity: 'vpc' (no regression)", () => {
|
|
216
|
+
test('MinCapacity defaults to 0.5 with no SecondsUntilAutoPause', async () => {
|
|
217
|
+
const t = await composeServerlessDefinition(makeApp());
|
|
218
|
+
const scaling = t.resources.Resources.FriggAuroraCluster.Properties.ServerlessV2ScalingConfiguration;
|
|
219
|
+
expect(scaling.MinCapacity).toBe(0.5);
|
|
220
|
+
expect(scaling.MaxCapacity).toBe(4);
|
|
221
|
+
expect(scaling.SecondsUntilAutoPause).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('Aurora ingress uses SourceSecurityGroupId (Lambda SG), not CidrIp', async () => {
|
|
225
|
+
const t = await composeServerlessDefinition(makeApp());
|
|
226
|
+
const ingress = findResources(
|
|
227
|
+
t,
|
|
228
|
+
(r) => r.Type === 'AWS::EC2::SecurityGroupIngress' && r.Properties.FromPort === 5432
|
|
229
|
+
);
|
|
230
|
+
expect(ingress).toHaveLength(1);
|
|
231
|
+
expect(ingress[0][1].Properties.SourceSecurityGroupId).toEqual({ Ref: 'FriggLambdaSecurityGroup' });
|
|
232
|
+
expect(ingress[0][1].Properties.CidrIp).toBeUndefined();
|
|
233
|
+
// Logical ID unchanged for the vpc path
|
|
234
|
+
expect(ingress[0][0]).toBe('FriggAuroraIngressRule');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('Aurora instance is not publicly accessible by default', async () => {
|
|
238
|
+
const t = await composeServerlessDefinition(makeApp());
|
|
239
|
+
expect(t.resources.Resources.FriggAuroraInstance.Properties.PubliclyAccessible).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('Lambda IS attached to the VPC (provider.vpc set)', async () => {
|
|
243
|
+
const t = await composeServerlessDefinition(makeApp());
|
|
244
|
+
expect(t.provider.vpc).toBeDefined();
|
|
245
|
+
expect(t.provider.vpc.subnetIds).toBeDefined();
|
|
246
|
+
expect(t.provider.vpc.securityGroupIds).toBeDefined();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('DATABASE_URL does not add sslmode in vpc mode', async () => {
|
|
250
|
+
const t = await composeServerlessDefinition(makeApp());
|
|
251
|
+
const url = t.provider.environment.DATABASE_URL;
|
|
252
|
+
expect(url['Fn::Sub'][0]).not.toContain('sslmode=require');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -94,21 +94,123 @@ class AuroraBuilder extends InfrastructureBuilder {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// Validate capacity settings
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
// minCapacity accepts 0 (Aurora Serverless v2 scale-to-zero, GA Nov 2024)
|
|
98
|
+
// OR a value in [0.5, 128]. Values in (0, 0.5) are not a valid Aurora
|
|
99
|
+
// capacity — reject them explicitly. See ADR-033.
|
|
100
|
+
if (
|
|
101
|
+
dbConfig.minCapacity !== undefined &&
|
|
102
|
+
dbConfig.minCapacity !== 0 &&
|
|
103
|
+
(dbConfig.minCapacity < 0.5 || dbConfig.minCapacity > 128)
|
|
104
|
+
) {
|
|
105
|
+
result.addError('database.postgres.minCapacity must be 0 (scale-to-zero) or between 0.5 and 128');
|
|
99
106
|
}
|
|
100
107
|
if (dbConfig.maxCapacity !== undefined && (dbConfig.maxCapacity < 0.5 || dbConfig.maxCapacity > 128)) {
|
|
101
108
|
result.addError('database.postgres.maxCapacity must be between 0.5 and 128');
|
|
102
109
|
}
|
|
103
110
|
|
|
111
|
+
// Validate scale-to-zero auto-pause window (seconds). AWS-valid range is
|
|
112
|
+
// 300–86400 (5 minutes to 24 hours) and it must be an integer.
|
|
113
|
+
if (dbConfig.secondsUntilAutoPause !== undefined) {
|
|
114
|
+
const s = dbConfig.secondsUntilAutoPause;
|
|
115
|
+
if (!Number.isInteger(s) || s < 300 || s > 86400) {
|
|
116
|
+
result.addError('database.postgres.secondsUntilAutoPause must be an integer between 300 and 86400');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Validate connectivity mode. 'vpc' (default) keeps today's behavior;
|
|
121
|
+
// 'public' places Aurora in public subnets and leaves the Lambda out of
|
|
122
|
+
// the VPC (NAT-free). See ADR-033.
|
|
123
|
+
if (dbConfig.connectivity !== undefined) {
|
|
124
|
+
const validConnectivity = ['vpc', 'public'];
|
|
125
|
+
if (!validConnectivity.includes(dbConfig.connectivity)) {
|
|
126
|
+
result.addError(
|
|
127
|
+
`Invalid database.postgres.connectivity: "${dbConfig.connectivity}". Must be one of: ${validConnectivity.join(', ')}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Validate allowedCidrs is an array of CIDR-looking strings when present.
|
|
133
|
+
if (dbConfig.allowedCidrs !== undefined) {
|
|
134
|
+
const cidrPattern = /^([0-9]{1,3}\.){3}[0-9]{1,3}\/[0-9]{1,2}$/;
|
|
135
|
+
if (!Array.isArray(dbConfig.allowedCidrs)) {
|
|
136
|
+
result.addError('database.postgres.allowedCidrs must be an array of CIDR strings');
|
|
137
|
+
} else {
|
|
138
|
+
const bad = dbConfig.allowedCidrs.filter(
|
|
139
|
+
(c) => typeof c !== 'string' || !cidrPattern.test(c)
|
|
140
|
+
);
|
|
141
|
+
if (bad.length > 0) {
|
|
142
|
+
result.addError(
|
|
143
|
+
`database.postgres.allowedCidrs contains invalid CIDR value(s): ${bad.join(', ')}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Scale-to-zero requires a supported engine version. Warn (do not
|
|
150
|
+
// hard-fail) if the user pins an older version with minCapacity: 0.
|
|
151
|
+
// Frigg's default engineVersion (15.13) qualifies. Minimum
|
|
152
|
+
// scale-to-zero-capable versions: Aurora PG 13.15 / 14.12 / 15.7 / 16.3.
|
|
153
|
+
if (dbConfig.minCapacity === 0 && dbConfig.engineVersion) {
|
|
154
|
+
if (!this.engineSupportsScaleToZero(dbConfig.engineVersion)) {
|
|
155
|
+
result.addWarning(
|
|
156
|
+
`database.postgres.engineVersion="${dbConfig.engineVersion}" may not support Aurora Serverless v2 scale-to-zero (minCapacity: 0). ` +
|
|
157
|
+
'Scale-to-zero requires Aurora PostgreSQL 13.15+/14.12+/15.7+/16.3+.'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
104
162
|
// Warn about public accessibility in production
|
|
105
163
|
if (dbConfig.publiclyAccessible === true) {
|
|
106
164
|
result.addWarning('database.postgres.publiclyAccessible=true is not recommended for production');
|
|
107
165
|
}
|
|
108
166
|
|
|
167
|
+
// ADR-033 security posture: public connectivity exposes the DB endpoint
|
|
168
|
+
// to the internet. Make the trade-off loud.
|
|
169
|
+
if (dbConfig.connectivity === 'public') {
|
|
170
|
+
result.addWarning(
|
|
171
|
+
'database.postgres.connectivity="public" exposes the Aurora endpoint to the internet. ' +
|
|
172
|
+
'TLS is enforced (sslmode=require) and access is restricted to allowedCidrs, but prefer "vpc" for production.'
|
|
173
|
+
);
|
|
174
|
+
const cidrs = Array.isArray(dbConfig.allowedCidrs) ? dbConfig.allowedCidrs : ['0.0.0.0/0'];
|
|
175
|
+
if (cidrs.includes('0.0.0.0/0')) {
|
|
176
|
+
result.addWarning(
|
|
177
|
+
'database.postgres.allowedCidrs allows 0.0.0.0/0 (the whole internet). ' +
|
|
178
|
+
'Narrow this to known egress ranges wherever the deployment can.'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
109
183
|
return result;
|
|
110
184
|
}
|
|
111
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Determine whether an Aurora PostgreSQL engine version supports
|
|
188
|
+
* Serverless v2 scale-to-zero (minCapacity: 0). Minimum capable versions:
|
|
189
|
+
* 13.15+, 14.12+, 15.7+, 16.3+. Unknown/unparseable versions return true
|
|
190
|
+
* (assume-capable) so we never hard-block on a version string we can't read;
|
|
191
|
+
* this only gates a warning, never a hard failure.
|
|
192
|
+
* @param {string} engineVersion e.g. '15.13'
|
|
193
|
+
* @returns {boolean}
|
|
194
|
+
*/
|
|
195
|
+
engineSupportsScaleToZero(engineVersion) {
|
|
196
|
+
const minByMajor = { 13: 15, 14: 12, 15: 7, 16: 3 };
|
|
197
|
+
const match = /^(\d+)\.(\d+)/.exec(String(engineVersion));
|
|
198
|
+
if (!match) {
|
|
199
|
+
return true; // can't parse — don't warn spuriously
|
|
200
|
+
}
|
|
201
|
+
const major = Number(match[1]);
|
|
202
|
+
const minor = Number(match[2]);
|
|
203
|
+
// Majors newer than the table are assumed capable.
|
|
204
|
+
if (major > 16) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
// Majors older than 13 never support scale-to-zero.
|
|
208
|
+
if (!(major in minByMajor)) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
return minor >= minByMajor[major];
|
|
212
|
+
}
|
|
213
|
+
|
|
112
214
|
/**
|
|
113
215
|
* Build Aurora infrastructure using ownership-based architecture
|
|
114
216
|
*/
|
|
@@ -327,16 +429,27 @@ class AuroraBuilder extends InfrastructureBuilder {
|
|
|
327
429
|
}
|
|
328
430
|
}
|
|
329
431
|
|
|
330
|
-
// Preserve other database config
|
|
331
|
-
|
|
432
|
+
// Preserve other database config.
|
|
433
|
+
// NOTE: use `!== undefined` (not truthiness) so a requested minCapacity
|
|
434
|
+
// of 0 (scale-to-zero) flows through instead of being dropped.
|
|
435
|
+
if (appDefinition.database?.postgres?.minCapacity !== undefined) {
|
|
332
436
|
translated.database.postgres.config.minCapacity = appDefinition.database.postgres.minCapacity;
|
|
333
437
|
}
|
|
334
|
-
if (appDefinition.database?.postgres?.maxCapacity) {
|
|
438
|
+
if (appDefinition.database?.postgres?.maxCapacity !== undefined) {
|
|
335
439
|
translated.database.postgres.config.maxCapacity = appDefinition.database.postgres.maxCapacity;
|
|
336
440
|
}
|
|
441
|
+
if (appDefinition.database?.postgres?.secondsUntilAutoPause !== undefined) {
|
|
442
|
+
translated.database.postgres.config.secondsUntilAutoPause = appDefinition.database.postgres.secondsUntilAutoPause;
|
|
443
|
+
}
|
|
337
444
|
if (appDefinition.database?.postgres?.publiclyAccessible !== undefined) {
|
|
338
445
|
translated.database.postgres.config.publiclyAccessible = appDefinition.database.postgres.publiclyAccessible;
|
|
339
446
|
}
|
|
447
|
+
if (appDefinition.database?.postgres?.connectivity !== undefined) {
|
|
448
|
+
translated.database.postgres.config.connectivity = appDefinition.database.postgres.connectivity;
|
|
449
|
+
}
|
|
450
|
+
if (appDefinition.database?.postgres?.allowedCidrs !== undefined) {
|
|
451
|
+
translated.database.postgres.config.allowedCidrs = appDefinition.database.postgres.allowedCidrs;
|
|
452
|
+
}
|
|
340
453
|
|
|
341
454
|
return translated;
|
|
342
455
|
}
|
|
@@ -373,7 +486,10 @@ class AuroraBuilder extends InfrastructureBuilder {
|
|
|
373
486
|
console.log(' Creating new Aurora Serverless v2 cluster...');
|
|
374
487
|
|
|
375
488
|
const dbConfig = appDefinition.database.postgres;
|
|
376
|
-
|
|
489
|
+
// ADR-033: connectivity 'public' implies a publicly-accessible cluster in
|
|
490
|
+
// public subnets. It combines with the legacy publiclyAccessible flag.
|
|
491
|
+
const publicConnectivity = dbConfig.connectivity === 'public';
|
|
492
|
+
const publiclyAccessible = publicConnectivity || dbConfig.publiclyAccessible === true;
|
|
377
493
|
|
|
378
494
|
// Get subnet IDs for DB Subnet Group
|
|
379
495
|
const subnetIds = publiclyAccessible
|
|
@@ -452,9 +568,16 @@ class AuroraBuilder extends InfrastructureBuilder {
|
|
|
452
568
|
// min when idle) and gives the DB enough headroom to
|
|
453
569
|
// absorb bursty sync traffic. Apps can still override both
|
|
454
570
|
// via app definition dbConfig.
|
|
571
|
+
// ADR-033: use nullish coalescing — `|| 0.5` silently turned a
|
|
572
|
+
// requested MinCapacity of 0 (scale-to-zero) back into 0.5.
|
|
573
|
+
// When MinCapacity is 0, emit SecondsUntilAutoPause so the cluster
|
|
574
|
+
// pauses to 0 ACU after the idle window (default 300s).
|
|
455
575
|
ServerlessV2ScalingConfiguration: {
|
|
456
|
-
MinCapacity: dbConfig.minCapacity
|
|
457
|
-
MaxCapacity: dbConfig.maxCapacity
|
|
576
|
+
MinCapacity: dbConfig.minCapacity ?? 0.5,
|
|
577
|
+
MaxCapacity: dbConfig.maxCapacity ?? 4,
|
|
578
|
+
...(dbConfig.minCapacity === 0
|
|
579
|
+
? { SecondsUntilAutoPause: dbConfig.secondsUntilAutoPause ?? 300 }
|
|
580
|
+
: {}),
|
|
458
581
|
},
|
|
459
582
|
EnableHttpEndpoint: false,
|
|
460
583
|
BackupRetentionPeriod: 7,
|
|
@@ -482,12 +605,15 @@ class AuroraBuilder extends InfrastructureBuilder {
|
|
|
482
605
|
},
|
|
483
606
|
};
|
|
484
607
|
|
|
485
|
-
// Environment variables
|
|
608
|
+
// Environment variables.
|
|
609
|
+
// ADR-033: public connectivity requires TLS (sslmode=require) since the
|
|
610
|
+
// endpoint is reachable over the internet.
|
|
486
611
|
result.environment.DATABASE_URL = this.buildDatabaseUrl(
|
|
487
612
|
{ 'Fn::GetAtt': ['FriggAuroraCluster', 'Endpoint.Address'] },
|
|
488
613
|
{ 'Fn::GetAtt': ['FriggAuroraCluster', 'Endpoint.Port'] },
|
|
489
614
|
dbConfig.database || 'frigg',
|
|
490
|
-
{ Ref: 'FriggDBSecret' }
|
|
615
|
+
{ Ref: 'FriggDBSecret' },
|
|
616
|
+
{ requireTls: publicConnectivity }
|
|
491
617
|
);
|
|
492
618
|
|
|
493
619
|
// IAM permissions for Secrets Manager
|
|
@@ -497,19 +623,46 @@ class AuroraBuilder extends InfrastructureBuilder {
|
|
|
497
623
|
Resource: { Ref: 'FriggDBSecret' },
|
|
498
624
|
});
|
|
499
625
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
626
|
+
if (publicConnectivity) {
|
|
627
|
+
// ADR-033 NAT-free public connectivity: the Lambda is NOT attached to
|
|
628
|
+
// the VPC (see vpc-builder), so FriggLambdaSecurityGroup is not on the
|
|
629
|
+
// Lambda side and a SourceSecurityGroupId rule would authorize nothing.
|
|
630
|
+
// Instead open 5432 to the configured CIDR allowlist (default
|
|
631
|
+
// 0.0.0.0/0 for demos — narrow it in production). One ingress rule per
|
|
632
|
+
// CIDR. GroupId stays FriggLambdaSecurityGroup because that is the SG
|
|
633
|
+
// attached to the Aurora cluster (see VpcSecurityGroupIds above).
|
|
634
|
+
const allowedCidrs =
|
|
635
|
+
Array.isArray(dbConfig.allowedCidrs) && dbConfig.allowedCidrs.length > 0
|
|
636
|
+
? dbConfig.allowedCidrs
|
|
637
|
+
: ['0.0.0.0/0'];
|
|
638
|
+
allowedCidrs.forEach((cidr, index) => {
|
|
639
|
+
result.resources[`FriggAuroraIngressRule${index}`] = {
|
|
640
|
+
Type: 'AWS::EC2::SecurityGroupIngress',
|
|
641
|
+
Properties: {
|
|
642
|
+
GroupId: { Ref: 'FriggLambdaSecurityGroup' },
|
|
643
|
+
IpProtocol: 'tcp',
|
|
644
|
+
FromPort: 5432,
|
|
645
|
+
ToPort: 5432,
|
|
646
|
+
CidrIp: cidr,
|
|
647
|
+
Description: `Allow PostgreSQL access from ${cidr} (public connectivity)`,
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
});
|
|
651
|
+
} else {
|
|
652
|
+
// Add self-referencing security group ingress rule to allow Lambda to connect to Aurora
|
|
653
|
+
// Since both Lambda and Aurora share the same security group, we need to allow the SG to accept traffic from itself
|
|
654
|
+
result.resources.FriggAuroraIngressRule = {
|
|
655
|
+
Type: 'AWS::EC2::SecurityGroupIngress',
|
|
656
|
+
Properties: {
|
|
657
|
+
GroupId: { Ref: 'FriggLambdaSecurityGroup' },
|
|
658
|
+
IpProtocol: 'tcp',
|
|
659
|
+
FromPort: 5432,
|
|
660
|
+
ToPort: 5432,
|
|
661
|
+
SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
|
|
662
|
+
Description: 'Allow Lambda functions to connect to Aurora PostgreSQL (self-referencing rule)',
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
}
|
|
513
666
|
|
|
514
667
|
console.log(' ✅ Aurora Serverless v2 cluster resources created');
|
|
515
668
|
}
|
|
@@ -554,6 +707,9 @@ class AuroraBuilder extends InfrastructureBuilder {
|
|
|
554
707
|
console.log(` ✅ Using discovered Aurora cluster: ${discoveredResources.auroraClusterEndpoint}`);
|
|
555
708
|
|
|
556
709
|
const dbConfig = appDefinition.database.postgres;
|
|
710
|
+
// ADR-033: public connectivity requires TLS on the connection string and a
|
|
711
|
+
// CIDR-based ingress rule (the Lambda is not VPC-attached).
|
|
712
|
+
const publicConnectivity = dbConfig.connectivity === 'public';
|
|
557
713
|
|
|
558
714
|
// Use discovered cluster details
|
|
559
715
|
result.environment.DATABASE_HOST = discoveredResources.auroraClusterEndpoint;
|
|
@@ -729,7 +885,8 @@ exports.handler = async (event, context) => {
|
|
|
729
885
|
discoveredResources.auroraClusterEndpoint,
|
|
730
886
|
discoveredResources.auroraPort || 5432,
|
|
731
887
|
dbConfig.database || 'frigg',
|
|
732
|
-
{ Ref: 'FriggDBSecret' }
|
|
888
|
+
{ Ref: 'FriggDBSecret' },
|
|
889
|
+
{ requireTls: publicConnectivity }
|
|
733
890
|
);
|
|
734
891
|
|
|
735
892
|
// Grant Lambda functions permission to read the secret
|
|
@@ -747,7 +904,8 @@ exports.handler = async (event, context) => {
|
|
|
747
904
|
discoveredResources.auroraClusterEndpoint,
|
|
748
905
|
discoveredResources.auroraPort || 5432,
|
|
749
906
|
dbConfig.database || 'frigg',
|
|
750
|
-
discoveredResources.databaseSecretArn
|
|
907
|
+
discoveredResources.databaseSecretArn,
|
|
908
|
+
{ requireTls: publicConnectivity }
|
|
751
909
|
);
|
|
752
910
|
|
|
753
911
|
result.iamStatements.push({
|
|
@@ -768,7 +926,10 @@ exports.handler = async (event, context) => {
|
|
|
768
926
|
// Consumers that build DATABASE_URL from components at runtime MUST
|
|
769
927
|
// append `?${DATABASE_URL_PARAMS}` to get the same hang-prevention
|
|
770
928
|
// timeouts as the managed path.
|
|
771
|
-
|
|
929
|
+
// ADR-033: public connectivity requires TLS — include sslmode=require.
|
|
930
|
+
result.environment.DATABASE_URL_PARAMS = publicConnectivity
|
|
931
|
+
? `${LAMBDA_DATABASE_URL_QUERY_PARAMS}&sslmode=require`
|
|
932
|
+
: LAMBDA_DATABASE_URL_QUERY_PARAMS;
|
|
772
933
|
|
|
773
934
|
// Note: DATABASE_URL is NOT set here to avoid Serverless variable resolution errors
|
|
774
935
|
// The application (Frigg Core) should construct it at runtime from:
|
|
@@ -782,17 +943,39 @@ exports.handler = async (event, context) => {
|
|
|
782
943
|
|
|
783
944
|
// Add security group ingress rule to allow Lambda to connect to Aurora
|
|
784
945
|
if (discoveredResources.auroraSecurityGroupId) {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
946
|
+
if (publicConnectivity) {
|
|
947
|
+
// ADR-033 NAT-free public connectivity: the Lambda is not in the
|
|
948
|
+
// VPC, so authorize the CIDR allowlist instead of the Lambda SG.
|
|
949
|
+
const allowedCidrs =
|
|
950
|
+
Array.isArray(dbConfig.allowedCidrs) && dbConfig.allowedCidrs.length > 0
|
|
951
|
+
? dbConfig.allowedCidrs
|
|
952
|
+
: ['0.0.0.0/0'];
|
|
953
|
+
allowedCidrs.forEach((cidr, index) => {
|
|
954
|
+
result.resources[`FriggAuroraIngressRule${index}`] = {
|
|
955
|
+
Type: 'AWS::EC2::SecurityGroupIngress',
|
|
956
|
+
Properties: {
|
|
957
|
+
GroupId: discoveredResources.auroraSecurityGroupId,
|
|
958
|
+
IpProtocol: 'tcp',
|
|
959
|
+
FromPort: discoveredResources.auroraPort || 5432,
|
|
960
|
+
ToPort: discoveredResources.auroraPort || 5432,
|
|
961
|
+
CidrIp: cidr,
|
|
962
|
+
Description: `Allow PostgreSQL access from ${cidr} (public connectivity)`,
|
|
963
|
+
},
|
|
964
|
+
};
|
|
965
|
+
});
|
|
966
|
+
} else {
|
|
967
|
+
result.resources.FriggAuroraIngressRule = {
|
|
968
|
+
Type: 'AWS::EC2::SecurityGroupIngress',
|
|
969
|
+
Properties: {
|
|
970
|
+
GroupId: discoveredResources.auroraSecurityGroupId,
|
|
971
|
+
IpProtocol: 'tcp',
|
|
972
|
+
FromPort: discoveredResources.auroraPort || 5432,
|
|
973
|
+
ToPort: discoveredResources.auroraPort || 5432,
|
|
974
|
+
SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
|
|
975
|
+
Description: 'Allow Lambda functions to connect to Aurora PostgreSQL',
|
|
976
|
+
},
|
|
977
|
+
};
|
|
978
|
+
}
|
|
796
979
|
console.log(` ✅ Added security group ingress rule for Lambda → Aurora connectivity`);
|
|
797
980
|
}
|
|
798
981
|
|
|
@@ -805,8 +988,11 @@ exports.handler = async (event, context) => {
|
|
|
805
988
|
* @param {string|number|object} port - Database port (string/number or CloudFormation intrinsic function)
|
|
806
989
|
* @param {string} database - Database name
|
|
807
990
|
* @param {string|object} secretRef - Secret ARN (string) or CloudFormation Ref object
|
|
991
|
+
* @param {object} [options]
|
|
992
|
+
* @param {boolean} [options.requireTls] - Append sslmode=require (ADR-033 public connectivity)
|
|
808
993
|
*/
|
|
809
|
-
buildDatabaseUrl(host, port, database, secretRef) {
|
|
994
|
+
buildDatabaseUrl(host, port, database, secretRef, options = {}) {
|
|
995
|
+
const { requireTls = false } = options;
|
|
810
996
|
// Handle secretRef as either a string ARN or CloudFormation Ref object
|
|
811
997
|
const resolveSecretRef = (secretRefValue) => {
|
|
812
998
|
if (typeof secretRefValue === 'object' && secretRefValue.Ref) {
|
|
@@ -838,9 +1024,16 @@ exports.handler = async (event, context) => {
|
|
|
838
1024
|
|
|
839
1025
|
// Query params are defined at module scope (LAMBDA_DATABASE_URL_QUERY_PARAMS)
|
|
840
1026
|
// so runtime-URL-construction paths can emit the same timeouts as an env var.
|
|
1027
|
+
// ADR-033: for public connectivity, TLS is mandatory — append sslmode=require
|
|
1028
|
+
// unless it's already present in the base params.
|
|
1029
|
+
const queryParams =
|
|
1030
|
+
requireTls && !/(^|&)sslmode=/.test(LAMBDA_DATABASE_URL_QUERY_PARAMS)
|
|
1031
|
+
? `${LAMBDA_DATABASE_URL_QUERY_PARAMS}&sslmode=require`
|
|
1032
|
+
: LAMBDA_DATABASE_URL_QUERY_PARAMS;
|
|
1033
|
+
|
|
841
1034
|
return {
|
|
842
1035
|
'Fn::Sub': [
|
|
843
|
-
`postgresql://\${Username}:\${Password}@\${Host}:\${Port}/\${Database}?${
|
|
1036
|
+
`postgresql://\${Username}:\${Password}@\${Host}:\${Port}/\${Database}?${queryParams}`,
|
|
844
1037
|
{
|
|
845
1038
|
Username: resolveSecretRef(secretRef),
|
|
846
1039
|
Password: resolveSecretPassword(secretRef),
|
|
@@ -24,6 +24,31 @@ const { createEmptyDiscoveryResult } = require('../shared/types/discovery-result
|
|
|
24
24
|
const { ResourceOwnership } = require('../shared/types/resource-ownership');
|
|
25
25
|
const { isSsmOffloadActive } = require('../parameters/offload-utils');
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* ADR-033: NAT-free public database connectivity.
|
|
29
|
+
*
|
|
30
|
+
* When the app opts into `database.postgres.connectivity: 'public'`, Aurora is
|
|
31
|
+
* placed in public subnets with a public endpoint and the app's Lambdas are
|
|
32
|
+
* intentionally left OUTSIDE the VPC so they keep default internet egress and
|
|
33
|
+
* need no NAT Gateway. This function is the single seam that DECOUPLES the
|
|
34
|
+
* Lambda's VPC attachment + NAT from Aurora's networking:
|
|
35
|
+
*
|
|
36
|
+
* - The VpcBuilder still builds the VPC / public subnets / DB subnet group
|
|
37
|
+
* that Aurora requires (Aurora must live in a VPC — an AWS constraint).
|
|
38
|
+
* - But it does NOT emit a NAT Gateway, and it clears `result.vpcConfig` so the
|
|
39
|
+
* composer never sets `provider.vpc` (see infrastructure-composer.js:126) —
|
|
40
|
+
* leaving the Lambda un-attached with normal egress.
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} appDefinition
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
function isPublicDatabaseConnectivity(appDefinition) {
|
|
46
|
+
return (
|
|
47
|
+
appDefinition?.database?.postgres?.enable === true &&
|
|
48
|
+
appDefinition?.database?.postgres?.connectivity === 'public'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
class VpcBuilder extends InfrastructureBuilder {
|
|
28
53
|
constructor() {
|
|
29
54
|
super();
|
|
@@ -565,11 +590,35 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
565
590
|
// Build Subnets based on ownership decision
|
|
566
591
|
this.buildSubnetsFromDecision(decisions.subnets, appDefinition, discoveredResources, result);
|
|
567
592
|
|
|
568
|
-
//
|
|
569
|
-
|
|
593
|
+
// ADR-033: NAT-free public database connectivity. When set, Aurora sits in
|
|
594
|
+
// public subnets with a public endpoint and the Lambda is NOT attached to
|
|
595
|
+
// the VPC — so it keeps default internet egress and needs no NAT Gateway.
|
|
596
|
+
// We still built the VPC + public subnets above (Aurora requires them), but
|
|
597
|
+
// we skip the NAT Gateway here and clear vpcConfig below so the composer
|
|
598
|
+
// leaves provider.vpc unset. This is the seam that decouples the Lambda's
|
|
599
|
+
// VPC attachment + NAT from Aurora's networking.
|
|
600
|
+
const publicDbConnectivity = isPublicDatabaseConnectivity(appDefinition);
|
|
601
|
+
|
|
602
|
+
if (publicDbConnectivity) {
|
|
603
|
+
console.log(
|
|
604
|
+
' ⊝ NAT Gateway skipped (database.postgres.connectivity=public — Lambda is not VPC-attached, so no NAT is needed)'
|
|
605
|
+
);
|
|
606
|
+
} else {
|
|
607
|
+
// Build NAT Gateway based on ownership decision
|
|
608
|
+
this.buildNatGatewayFromDecision(decisions.natGateway, appDefinition, discoveredResources, result);
|
|
609
|
+
}
|
|
570
610
|
|
|
571
|
-
// Build VPC Endpoints based on ownership decisions
|
|
572
|
-
|
|
611
|
+
// Build VPC Endpoints based on ownership decisions.
|
|
612
|
+
// ADR-033: in public connectivity the Lambda is not in the VPC, so VPC
|
|
613
|
+
// endpoints (which give in-VPC functions private AWS access) would be
|
|
614
|
+
// wasted spend — skip them along with the NAT Gateway.
|
|
615
|
+
if (publicDbConnectivity) {
|
|
616
|
+
console.log(
|
|
617
|
+
' ⊝ VPC Endpoints skipped (database.postgres.connectivity=public — Lambda is not VPC-attached)'
|
|
618
|
+
);
|
|
619
|
+
} else {
|
|
620
|
+
this.buildVpcEndpointsFromDecisions(decisions.vpcEndpoints, decisions.securityGroup, appDefinition, discoveredResources, result);
|
|
621
|
+
}
|
|
573
622
|
|
|
574
623
|
// Set VPC_ENABLED environment variable
|
|
575
624
|
result.environment.VPC_ENABLED = 'true';
|
|
@@ -579,6 +628,18 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
579
628
|
console.log(` - Subnets: ${result.vpcConfig.subnetIds.length}`);
|
|
580
629
|
console.log(` - Security Groups: ${result.vpcConfig.securityGroupIds.length}`);
|
|
581
630
|
|
|
631
|
+
if (publicDbConnectivity) {
|
|
632
|
+
// Do NOT attach the Lambda to the VPC: leaving vpcConfig null means the
|
|
633
|
+
// composer never sets provider.vpc, so the function keeps normal
|
|
634
|
+
// internet egress. The Aurora subnets/DB subnet group built above are
|
|
635
|
+
// still emitted (AuroraBuilder consumes discoveredResources.publicSubnetId*),
|
|
636
|
+
// and the Lambda reaches Aurora over its public endpoint + TLS.
|
|
637
|
+
console.log(
|
|
638
|
+
' ⊝ Lambda VPC attachment skipped (database.postgres.connectivity=public — provider.vpc will be unset)'
|
|
639
|
+
);
|
|
640
|
+
result.vpcConfig = null;
|
|
641
|
+
}
|
|
642
|
+
|
|
582
643
|
return result;
|
|
583
644
|
}
|
|
584
645
|
|
|
@@ -55,10 +55,26 @@
|
|
|
55
55
|
*
|
|
56
56
|
* @property {Object} [config] - Configuration preferences
|
|
57
57
|
* @property {'aurora-postgresql'|'aurora-mysql'} [config.engine] - Database engine
|
|
58
|
-
* @property {number} [config.minCapacity] - Min serverless capacity
|
|
59
|
-
*
|
|
58
|
+
* @property {number} [config.minCapacity] - Min serverless capacity in ACU. Accepts
|
|
59
|
+
* `0` for Aurora Serverless v2 scale-to-zero (cluster pauses to 0 ACU when idle;
|
|
60
|
+
* requires Aurora PostgreSQL 13.15+/14.12+/15.7+/16.3+, and the default 15.13
|
|
61
|
+
* qualifies) OR a value in [0.5, 128]. Values in (0, 0.5) are invalid. (default: 0.5)
|
|
62
|
+
* @property {number} [config.maxCapacity] - Max serverless capacity in ACU, range
|
|
63
|
+
* [0.5, 128] (default: 4)
|
|
64
|
+
* @property {number} [config.secondsUntilAutoPause] - Idle window before a
|
|
65
|
+
* scale-to-zero cluster (minCapacity: 0) pauses to 0 ACU. Integer seconds in
|
|
66
|
+
* [300, 86400]. Only applied when minCapacity === 0 (default: 300)
|
|
60
67
|
* @property {string} [config.database] - Database name (default: 'frigg')
|
|
61
68
|
* @property {boolean} [config.publiclyAccessible] - Public access (default: false)
|
|
69
|
+
* @property {'vpc'|'public'} [config.connectivity] - Lambda↔Aurora connectivity
|
|
70
|
+
* mode. `'vpc'` (default): Aurora in private subnets, Lambda attached to the VPC,
|
|
71
|
+
* ingress from the Lambda security group (needs a NAT for external egress).
|
|
72
|
+
* `'public'` (NAT-free): Aurora in public subnets with a public endpoint, Lambda
|
|
73
|
+
* left OUTSIDE the VPC (no NAT Gateway, no VPC endpoints), ingress opened to
|
|
74
|
+
* `allowedCidrs`, TLS enforced (sslmode=require). (default: 'vpc')
|
|
75
|
+
* @property {string[]} [config.allowedCidrs] - CIDR blocks allowed to reach Aurora
|
|
76
|
+
* on 5432 when connectivity is 'public'. One ingress rule is emitted per CIDR.
|
|
77
|
+
* (default: ['0.0.0.0/0'] — narrow this for production)
|
|
62
78
|
* @property {boolean} [config.autoCreateCredentials] - Auto-create credentials in Secrets Manager
|
|
63
79
|
*/
|
|
64
80
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/devtools",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
4
|
+
"version": "2.0.0--canary.640.b31eb4a.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"frigg": "./frigg-cli/index.js"
|
|
7
7
|
},
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"@babel/eslint-parser": "^7.18.9",
|
|
27
27
|
"@babel/parser": "^7.25.3",
|
|
28
28
|
"@babel/traverse": "^7.25.3",
|
|
29
|
-
"@friggframework/core": "2.0.0--canary.
|
|
30
|
-
"@friggframework/schemas": "2.0.0--canary.
|
|
31
|
-
"@friggframework/test": "2.0.0--canary.
|
|
29
|
+
"@friggframework/core": "2.0.0--canary.640.b31eb4a.0",
|
|
30
|
+
"@friggframework/schemas": "2.0.0--canary.640.b31eb4a.0",
|
|
31
|
+
"@friggframework/test": "2.0.0--canary.640.b31eb4a.0",
|
|
32
32
|
"@hapi/boom": "^10.0.1",
|
|
33
33
|
"@inquirer/prompts": "^5.3.8",
|
|
34
34
|
"axios": "^1.18.0",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"validate-npm-package-name": "^5.0.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
60
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
59
|
+
"@friggframework/eslint-config": "2.0.0--canary.640.b31eb4a.0",
|
|
60
|
+
"@friggframework/prettier-config": "2.0.0--canary.640.b31eb4a.0",
|
|
61
61
|
"aws-sdk-client-mock": "^4.1.0",
|
|
62
62
|
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
63
63
|
"jest": "^30.1.3",
|
|
@@ -89,5 +89,5 @@
|
|
|
89
89
|
"publishConfig": {
|
|
90
90
|
"access": "public"
|
|
91
91
|
},
|
|
92
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "b31eb4a3ddaeae1044d98050b6a436f6d9c08db6"
|
|
93
93
|
}
|