@fjall/components-infrastructure 5.0.0 → 7.0.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/config/aws/ebsDefaultEncryption.js +1 -1
- package/dist/lib/config/aws/inspectorEnablement.js +1 -1
- package/dist/lib/config/aws/oidcConnector.js +1 -1
- package/dist/lib/config/aws/s3BlockPublicAccess.js +1 -1
- package/dist/lib/config/aws/securityServicesAdmin.js +2 -2
- package/dist/lib/lambda-assets/cert-generator/asset/index.js +31 -31
- package/dist/lib/patterns/aws/clickhouseDatabase.js +4 -0
- package/dist/lib/patterns/aws/compute.js +1 -1
- package/dist/lib/patterns/aws/computeEcs.js +5 -0
- package/dist/lib/patterns/aws/computeLambda.d.ts +1 -1
- package/dist/lib/patterns/aws/devSubstrate.js +1 -1
- package/dist/lib/patterns/aws/payload.js +3 -3
- package/dist/lib/patterns/aws/staticSite.js +1 -1
- package/dist/lib/resources/aws/compute/ec2.d.ts +31 -14
- package/dist/lib/resources/aws/compute/ec2.js +34 -10
- package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.js +1 -1
- package/dist/lib/resources/aws/compute/ecsCapacityConfig.d.ts +83 -0
- package/dist/lib/resources/aws/compute/ecsCapacityConfig.js +323 -0
- package/dist/lib/resources/aws/compute/ecsConstants.d.ts +19 -0
- package/dist/lib/resources/aws/compute/ecsConstants.js +24 -0
- package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.js +1 -1
- package/dist/lib/resources/aws/compute/ecsServiceFactory.d.ts +20 -16
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +73 -118
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +33 -4
- package/dist/lib/resources/aws/compute/ecsValidation.js +6 -0
- package/dist/lib/resources/aws/compute/persistentDataVolume.d.ts +15 -5
- package/dist/lib/resources/aws/compute/persistentDataVolume.js +10 -7
- package/dist/lib/resources/aws/database/rdsInstance.js +1 -1
- package/dist/lib/resources/aws/iam/identityCenter/user.js +1 -1
- package/dist/lib/resources/aws/networking/crossAccountReturnRoutes.js +1 -1
- package/dist/lib/resources/aws/networking/ipamPool.js +1 -1
- package/dist/lib/resources/aws/organisation/costAllocationTagActivator.js +1 -1
- package/dist/lib/resources/aws/utilities/tlsCertGenerator.js +1 -1
- package/dist/lib/utils/capacityIdentityContext.d.ts +20 -0
- package/dist/lib/utils/capacityIdentityContext.js +43 -0
- package/dist/lib/utils/engineCompat.d.ts +12 -5
- package/dist/lib/utils/engineCompat.js +41 -22
- package/dist/lib/utils/manifestWriter.d.ts +19 -2
- package/dist/lib/utils/manifestWriter.js +35 -0
- package/package.json +10 -6
- package/dist/lib/config/aws/identityCenterMembership.d.ts +0 -11
- package/dist/lib/config/aws/identityCenterMembership.js +0 -61
- package/dist/lib/layers/layers/secrets-resolver/bin/resolve-secrets +0 -30
- package/dist/lib/layers/layers/secrets-resolver/bin/resolve-secrets.mjs +0 -212
- package/dist/lib/patterns/aws/buildkite/alarms.d.ts +0 -25
- package/dist/lib/patterns/aws/buildkite/alarms.js +0 -78
- package/dist/lib/patterns/aws/domainDelegation.d.ts +0 -8
- package/dist/lib/patterns/aws/domainDelegation.js +0 -45
- package/dist/lib/patterns/aws/domainFactory.d.ts +0 -23
- package/dist/lib/patterns/aws/domainFactory.js +0 -61
- package/dist/lib/utils/addSuffixToEmail.d.ts +0 -1
- package/dist/lib/utils/addSuffixToEmail.js +0 -3
|
@@ -11,7 +11,7 @@ export class EbsDefaultEncryption extends Construct {
|
|
|
11
11
|
constructor(scope, id) {
|
|
12
12
|
super(scope, id);
|
|
13
13
|
new CustomResource(this, "EbsDefaultEncryption", {
|
|
14
|
-
runtime: Runtime.
|
|
14
|
+
runtime: Runtime.NODEJS_24_X,
|
|
15
15
|
timeout: Duration.minutes(5),
|
|
16
16
|
lambdaDescription: "Enables EBS default encryption at account level",
|
|
17
17
|
inlinePolicy: [
|
|
@@ -12,7 +12,7 @@ export class InspectorEnablement extends Construct {
|
|
|
12
12
|
constructor(scope, id) {
|
|
13
13
|
super(scope, id);
|
|
14
14
|
new CustomResource(this, "InspectorEnable", {
|
|
15
|
-
runtime: Runtime.
|
|
15
|
+
runtime: Runtime.NODEJS_24_X,
|
|
16
16
|
timeout: Duration.minutes(5),
|
|
17
17
|
lambdaDescription: "Enables/disables Inspector v2 for the account",
|
|
18
18
|
inlinePolicy: [
|
|
@@ -74,7 +74,7 @@ export class OidcConnector extends Construct {
|
|
|
74
74
|
const issuerDomain = FJALL_OIDC_ISSUER_DOMAIN;
|
|
75
75
|
const issuerUrl = `https://${issuerDomain}`;
|
|
76
76
|
const providerResource = new CustomResource(this, "OidcProvider", {
|
|
77
|
-
runtime: Runtime.
|
|
77
|
+
runtime: Runtime.NODEJS_24_X,
|
|
78
78
|
timeout: Duration.minutes(5),
|
|
79
79
|
lambdaDescription: "Adopt-or-create the account-global Fjall OIDC provider",
|
|
80
80
|
resourceType: "Custom::FjallOidcProvider",
|
|
@@ -25,7 +25,7 @@ export class S3BlockPublicAccess extends Construct {
|
|
|
25
25
|
const account = getConfig().providerAccounts.find((pa) => pa.id === props.accountId);
|
|
26
26
|
const enforce = (account?.s3BlockPublicAccess ?? "off") === "enforced";
|
|
27
27
|
new CustomResource(this, "S3BlockPublicAccess", {
|
|
28
|
-
runtime: Runtime.
|
|
28
|
+
runtime: Runtime.NODEJS_24_X,
|
|
29
29
|
timeout: Duration.minutes(5),
|
|
30
30
|
lambdaDescription: "Manages S3 Block Public Access at account level",
|
|
31
31
|
rolePath: "/fjall/",
|
|
@@ -35,7 +35,7 @@ export class SecurityServicesAdmin extends Construct {
|
|
|
35
35
|
}
|
|
36
36
|
createGuardDutyAdmin() {
|
|
37
37
|
new CustomResource(this, "GuardDutyOrgConfig", {
|
|
38
|
-
runtime: Runtime.
|
|
38
|
+
runtime: Runtime.NODEJS_24_X,
|
|
39
39
|
timeout: Duration.minutes(5),
|
|
40
40
|
lambdaDescription: "Configures GuardDuty organisation-wide auto-enablement",
|
|
41
41
|
inlinePolicy: [
|
|
@@ -70,7 +70,7 @@ exports.handler = async (event) => {
|
|
|
70
70
|
}
|
|
71
71
|
createSecurityHubAdmin() {
|
|
72
72
|
new CustomResource(this, "SecurityHubOrgConfig", {
|
|
73
|
-
runtime: Runtime.
|
|
73
|
+
runtime: Runtime.NODEJS_24_X,
|
|
74
74
|
timeout: Duration.minutes(5),
|
|
75
75
|
lambdaDescription: "Configures SecurityHub organisation-wide auto-enablement",
|
|
76
76
|
inlinePolicy: [
|
|
@@ -6549,15 +6549,15 @@ var require_parser = __commonJS({
|
|
|
6549
6549
|
var helper_1 = require_helper();
|
|
6550
6550
|
var storage_1 = require_storage();
|
|
6551
6551
|
var AsnParser = class {
|
|
6552
|
-
static parse(data, target) {
|
|
6553
|
-
const asn1Parsed = asn1js.fromBER((0, bytes_1.toArrayBuffer)(data));
|
|
6552
|
+
static parse(data, target, options) {
|
|
6553
|
+
const asn1Parsed = asn1js.fromBER((0, bytes_1.toArrayBuffer)(data), options?.berOptions);
|
|
6554
6554
|
if (asn1Parsed.result.error) {
|
|
6555
6555
|
throw new Error(asn1Parsed.result.error);
|
|
6556
6556
|
}
|
|
6557
|
-
const res = this.fromASN(asn1Parsed.result, target);
|
|
6557
|
+
const res = this.fromASN(asn1Parsed.result, target, options);
|
|
6558
6558
|
return res;
|
|
6559
6559
|
}
|
|
6560
|
-
static fromASN(asn1Schema, target) {
|
|
6560
|
+
static fromASN(asn1Schema, target, options) {
|
|
6561
6561
|
try {
|
|
6562
6562
|
if ((0, helper_1.isConvertible)(target)) {
|
|
6563
6563
|
const value = new target();
|
|
@@ -6566,7 +6566,7 @@ var require_parser = __commonJS({
|
|
|
6566
6566
|
const schema = storage_1.schemaStorage.get(target);
|
|
6567
6567
|
storage_1.schemaStorage.cache(target);
|
|
6568
6568
|
let targetSchema = schema.schema;
|
|
6569
|
-
const choiceResult = this.handleChoiceTypes(asn1Schema, schema, target, targetSchema);
|
|
6569
|
+
const choiceResult = this.handleChoiceTypes(asn1Schema, schema, target, targetSchema, options);
|
|
6570
6570
|
if (choiceResult?.result) {
|
|
6571
6571
|
return choiceResult.result;
|
|
6572
6572
|
}
|
|
@@ -6576,9 +6576,9 @@ var require_parser = __commonJS({
|
|
|
6576
6576
|
const sequenceResult = this.handleSequenceTypes(asn1Schema, schema, target, targetSchema);
|
|
6577
6577
|
const res = new target();
|
|
6578
6578
|
if ((0, helper_1.isTypeOfArray)(target)) {
|
|
6579
|
-
return this.handleArrayTypes(asn1Schema, schema, target);
|
|
6579
|
+
return this.handleArrayTypes(asn1Schema, schema, target, options);
|
|
6580
6580
|
}
|
|
6581
|
-
this.processSchemaItems(schema, sequenceResult, res);
|
|
6581
|
+
this.processSchemaItems(schema, sequenceResult, res, options);
|
|
6582
6582
|
return res;
|
|
6583
6583
|
} catch (error) {
|
|
6584
6584
|
if (error instanceof errors_1.AsnSchemaValidationError) {
|
|
@@ -6587,7 +6587,7 @@ var require_parser = __commonJS({
|
|
|
6587
6587
|
throw error;
|
|
6588
6588
|
}
|
|
6589
6589
|
}
|
|
6590
|
-
static handleChoiceTypes(asn1Schema, schema, target, targetSchema) {
|
|
6590
|
+
static handleChoiceTypes(asn1Schema, schema, target, targetSchema, options) {
|
|
6591
6591
|
if (asn1Schema.constructor === asn1js.Constructed && schema.type === enums_1.AsnTypeTypes.Choice && asn1Schema.idBlock.tagClass === 3) {
|
|
6592
6592
|
for (const key in schema.items) {
|
|
6593
6593
|
const schemaItem = schema.items[key];
|
|
@@ -6598,7 +6598,7 @@ var require_parser = __commonJS({
|
|
|
6598
6598
|
const newSeq = new asn1js.Sequence();
|
|
6599
6599
|
if ("value" in asn1Schema.valueBlock && Array.isArray(asn1Schema.valueBlock.value) && "value" in newSeq.valueBlock) {
|
|
6600
6600
|
newSeq.valueBlock.value = asn1Schema.valueBlock.value;
|
|
6601
|
-
const fieldValue = this.fromASN(newSeq, schemaItem.type);
|
|
6601
|
+
const fieldValue = this.fromASN(newSeq, schemaItem.type, options);
|
|
6602
6602
|
const res = new target();
|
|
6603
6603
|
res[key] = fieldValue;
|
|
6604
6604
|
return { result: res };
|
|
@@ -6689,7 +6689,7 @@ var require_parser = __commonJS({
|
|
|
6689
6689
|
throw err;
|
|
6690
6690
|
}
|
|
6691
6691
|
}
|
|
6692
|
-
static handleArrayTypes(asn1Schema, schema, target) {
|
|
6692
|
+
static handleArrayTypes(asn1Schema, schema, target, options) {
|
|
6693
6693
|
if (!("value" in asn1Schema.valueBlock && Array.isArray(asn1Schema.valueBlock.value))) {
|
|
6694
6694
|
throw new Error("Cannot get items from the ASN.1 parsed value. ASN.1 object is not constructed.");
|
|
6695
6695
|
}
|
|
@@ -6701,10 +6701,10 @@ var require_parser = __commonJS({
|
|
|
6701
6701
|
}
|
|
6702
6702
|
return target.from(asn1Schema.valueBlock.value, (element) => converter.fromASN(element));
|
|
6703
6703
|
} else {
|
|
6704
|
-
return target.from(asn1Schema.valueBlock.value, (element) => this.fromASN(element, itemType));
|
|
6704
|
+
return target.from(asn1Schema.valueBlock.value, (element) => this.fromASN(element, itemType, options));
|
|
6705
6705
|
}
|
|
6706
6706
|
}
|
|
6707
|
-
static processSchemaItems(schema, asn1ComparedSchema, res) {
|
|
6707
|
+
static processSchemaItems(schema, asn1ComparedSchema, res, options) {
|
|
6708
6708
|
for (const key in schema.items) {
|
|
6709
6709
|
const asn1SchemaValue = asn1ComparedSchema.result[key];
|
|
6710
6710
|
if (!asn1SchemaValue) {
|
|
@@ -6714,9 +6714,9 @@ var require_parser = __commonJS({
|
|
|
6714
6714
|
const schemaItemType = schemaItem.type;
|
|
6715
6715
|
let parsedValue;
|
|
6716
6716
|
if (typeof schemaItemType === "number" || (0, helper_1.isConvertible)(schemaItemType)) {
|
|
6717
|
-
parsedValue = this.processPrimitiveSchemaItem(asn1SchemaValue, schemaItem, schemaItemType);
|
|
6717
|
+
parsedValue = this.processPrimitiveSchemaItem(asn1SchemaValue, schemaItem, schemaItemType, options);
|
|
6718
6718
|
} else {
|
|
6719
|
-
parsedValue = this.processComplexSchemaItem(asn1SchemaValue, schemaItem, schemaItemType);
|
|
6719
|
+
parsedValue = this.processComplexSchemaItem(asn1SchemaValue, schemaItem, schemaItemType, options);
|
|
6720
6720
|
}
|
|
6721
6721
|
if (parsedValue && typeof parsedValue === "object" && "value" in parsedValue && "raw" in parsedValue) {
|
|
6722
6722
|
res[key] = parsedValue.value;
|
|
@@ -6726,23 +6726,23 @@ var require_parser = __commonJS({
|
|
|
6726
6726
|
}
|
|
6727
6727
|
}
|
|
6728
6728
|
}
|
|
6729
|
-
static processPrimitiveSchemaItem(asn1SchemaValue, schemaItem, schemaItemType) {
|
|
6729
|
+
static processPrimitiveSchemaItem(asn1SchemaValue, schemaItem, schemaItemType, options) {
|
|
6730
6730
|
const converter = schemaItem.converter ?? ((0, helper_1.isConvertible)(schemaItemType) ? new schemaItemType() : null);
|
|
6731
6731
|
if (!converter) {
|
|
6732
6732
|
throw new Error("Converter is empty");
|
|
6733
6733
|
}
|
|
6734
6734
|
if (schemaItem.repeated) {
|
|
6735
|
-
return this.processRepeatedPrimitiveItem(asn1SchemaValue, schemaItem, converter);
|
|
6735
|
+
return this.processRepeatedPrimitiveItem(asn1SchemaValue, schemaItem, converter, options);
|
|
6736
6736
|
} else {
|
|
6737
|
-
return this.processSinglePrimitiveItem(asn1SchemaValue, schemaItem, schemaItemType, converter);
|
|
6737
|
+
return this.processSinglePrimitiveItem(asn1SchemaValue, schemaItem, schemaItemType, converter, options);
|
|
6738
6738
|
}
|
|
6739
6739
|
}
|
|
6740
|
-
static processRepeatedPrimitiveItem(asn1SchemaValue, schemaItem, converter) {
|
|
6740
|
+
static processRepeatedPrimitiveItem(asn1SchemaValue, schemaItem, converter, options) {
|
|
6741
6741
|
if (schemaItem.implicit) {
|
|
6742
6742
|
const Container = schemaItem.repeated === "sequence" ? asn1js.Sequence : asn1js.Set;
|
|
6743
6743
|
const newItem = new Container();
|
|
6744
6744
|
newItem.valueBlock = asn1SchemaValue.valueBlock;
|
|
6745
|
-
const newItemAsn = asn1js.fromBER(newItem.toBER(false));
|
|
6745
|
+
const newItemAsn = asn1js.fromBER(newItem.toBER(false), options?.berOptions);
|
|
6746
6746
|
if (newItemAsn.offset === -1) {
|
|
6747
6747
|
throw new Error(`Cannot parse the child item. ${newItemAsn.result.error}`);
|
|
6748
6748
|
}
|
|
@@ -6755,7 +6755,7 @@ var require_parser = __commonJS({
|
|
|
6755
6755
|
return Array.from(asn1SchemaValue, (element) => converter.fromASN(element));
|
|
6756
6756
|
}
|
|
6757
6757
|
}
|
|
6758
|
-
static processSinglePrimitiveItem(asn1SchemaValue, schemaItem, schemaItemType, converter) {
|
|
6758
|
+
static processSinglePrimitiveItem(asn1SchemaValue, schemaItem, schemaItemType, converter, options) {
|
|
6759
6759
|
let value = asn1SchemaValue;
|
|
6760
6760
|
if (schemaItem.implicit) {
|
|
6761
6761
|
let newItem;
|
|
@@ -6770,21 +6770,21 @@ var require_parser = __commonJS({
|
|
|
6770
6770
|
newItem = new Asn1Type();
|
|
6771
6771
|
}
|
|
6772
6772
|
newItem.valueBlock = value.valueBlock;
|
|
6773
|
-
value = asn1js.fromBER(newItem.toBER(false)).result;
|
|
6773
|
+
value = asn1js.fromBER(newItem.toBER(false), options?.berOptions).result;
|
|
6774
6774
|
}
|
|
6775
6775
|
return converter.fromASN(value);
|
|
6776
6776
|
}
|
|
6777
|
-
static processComplexSchemaItem(asn1SchemaValue, schemaItem, schemaItemType) {
|
|
6777
|
+
static processComplexSchemaItem(asn1SchemaValue, schemaItem, schemaItemType, options) {
|
|
6778
6778
|
if (schemaItem.repeated) {
|
|
6779
6779
|
if (!Array.isArray(asn1SchemaValue)) {
|
|
6780
6780
|
throw new Error("Cannot get list of items from the ASN.1 parsed value. ASN.1 value should be iterable.");
|
|
6781
6781
|
}
|
|
6782
|
-
return Array.from(asn1SchemaValue, (element) => this.fromASN(element, schemaItemType));
|
|
6782
|
+
return Array.from(asn1SchemaValue, (element) => this.fromASN(element, schemaItemType, options));
|
|
6783
6783
|
} else {
|
|
6784
6784
|
const valueToProcess = this.handleImplicitTagging(asn1SchemaValue, schemaItem, schemaItemType);
|
|
6785
6785
|
if (this.isOptionalChoiceField(schemaItem)) {
|
|
6786
6786
|
try {
|
|
6787
|
-
return this.fromASN(valueToProcess, schemaItemType);
|
|
6787
|
+
return this.fromASN(valueToProcess, schemaItemType, options);
|
|
6788
6788
|
} catch (err) {
|
|
6789
6789
|
if (err instanceof errors_1.AsnSchemaValidationError && /Wrong values for Choice type/.test(err.message)) {
|
|
6790
6790
|
return void 0;
|
|
@@ -6792,7 +6792,7 @@ var require_parser = __commonJS({
|
|
|
6792
6792
|
throw err;
|
|
6793
6793
|
}
|
|
6794
6794
|
} else {
|
|
6795
|
-
const parsedValue = this.fromASN(valueToProcess, schemaItemType);
|
|
6795
|
+
const parsedValue = this.fromASN(valueToProcess, schemaItemType, options);
|
|
6796
6796
|
if (schemaItem.raw) {
|
|
6797
6797
|
return {
|
|
6798
6798
|
value: parsedValue,
|
|
@@ -7009,12 +7009,12 @@ var require_convert = __commonJS({
|
|
|
7009
7009
|
static serialize(obj) {
|
|
7010
7010
|
return serializer_1.AsnSerializer.serialize(obj);
|
|
7011
7011
|
}
|
|
7012
|
-
static parse(data, target) {
|
|
7013
|
-
return parser_1.AsnParser.parse(data, target);
|
|
7012
|
+
static parse(data, target, options) {
|
|
7013
|
+
return parser_1.AsnParser.parse(data, target, options);
|
|
7014
7014
|
}
|
|
7015
|
-
static toString(data) {
|
|
7015
|
+
static toString(data, options) {
|
|
7016
7016
|
const buf = (0, bytes_1.isBufferSource)(data) ? (0, bytes_1.toArrayBuffer)(data) : _AsnConvert.serialize(data);
|
|
7017
|
-
const asn = asn1js.fromBER(buf);
|
|
7017
|
+
const asn = asn1js.fromBER(buf, options?.berOptions);
|
|
7018
7018
|
if (asn.offset === -1) {
|
|
7019
7019
|
throw new Error(`Cannot decode ASN.1 data. ${asn.result.error}`);
|
|
7020
7020
|
}
|
|
@@ -14678,9 +14678,9 @@ var require_cjs11 = __commonJS({
|
|
|
14678
14678
|
}
|
|
14679
14679
|
});
|
|
14680
14680
|
|
|
14681
|
-
// node_modules/@peculiar/x509/build/x509.cjs.js
|
|
14681
|
+
// ../../node_modules/@peculiar/x509/build/x509.cjs.js
|
|
14682
14682
|
var require_x509_cjs = __commonJS({
|
|
14683
|
-
"node_modules/@peculiar/x509/build/x509.cjs.js"(exports2) {
|
|
14683
|
+
"../../node_modules/@peculiar/x509/build/x509.cjs.js"(exports2) {
|
|
14684
14684
|
"use strict";
|
|
14685
14685
|
var asn1Schema = require_cjs();
|
|
14686
14686
|
var asn1X509 = require_cjs2();
|
|
@@ -389,6 +389,10 @@ export class ClickHouseDatabase extends Construct {
|
|
|
389
389
|
securityGroups: [securityGroup],
|
|
390
390
|
deployment: { minHealthyPercent: 0, maxHealthyPercent: 100 },
|
|
391
391
|
ec2Config: {
|
|
392
|
+
// Pattern-chosen stable slot: the composed cluster must never
|
|
393
|
+
// collide with the app's own EC2 compute defaulting "primary"
|
|
394
|
+
// (slots are app-scoped; collision is a hard synth error).
|
|
395
|
+
slot: "clickhouse",
|
|
392
396
|
instanceType,
|
|
393
397
|
machineImage: EcsOptimizedImage.amazonLinux2023(amiHardwareType),
|
|
394
398
|
instanceMonitoring: Monitoring.BASIC,
|
|
@@ -26,6 +26,7 @@ import { VALIDATION_PATTERNS } from "@fjall/generator";
|
|
|
26
26
|
import { evaluateBakeGuard } from "@fjall/util/docker";
|
|
27
27
|
import { toKebab, buildParameterPath } from "@fjall/util";
|
|
28
28
|
import { validateEc2ServiceSizing, validateEcsDomainConfig, validateSecretName } from "../../resources/aws/compute/ecsValidation.js";
|
|
29
|
+
import { validateSharedEc2CapacityConfig } from "../../resources/aws/compute/ecsCapacityConfig.js";
|
|
29
30
|
import { COMPUTE_DEFAULTS, collectImportedSecretNames } from "./compute.js";
|
|
30
31
|
import { isHookMigrations } from "./computeEcsTypes.js";
|
|
31
32
|
export { ScalingType } from "./computeEcsTypes.js";
|
|
@@ -174,6 +175,10 @@ export function validateEcsProps(props) {
|
|
|
174
175
|
validateSeparateTaskDef(service.name, service.migrations.separateTaskDef);
|
|
175
176
|
}
|
|
176
177
|
}
|
|
178
|
+
// Mirror of the resources-layer shared-ASG capacity check (drift on a reused
|
|
179
|
+
// ASG, shared persistentDataVolume singleton) — defence-in-depth per
|
|
180
|
+
// generator-standards § "Validate at the Lowest Layer the Field Belongs To".
|
|
181
|
+
validateSharedEc2CapacityConfig(props.services);
|
|
177
182
|
// Validate awaitMigrationsFrom cross-service migration ordering.
|
|
178
183
|
const serviceByName = new Map(props.services.map((s) => [s.name, s]));
|
|
179
184
|
for (const service of props.services) {
|
|
@@ -201,7 +201,7 @@ export interface CodeLambdaProps extends BaseLambdaProps {
|
|
|
201
201
|
code: Code;
|
|
202
202
|
/** Handler function. Default: "index.handler" */
|
|
203
203
|
handler?: string;
|
|
204
|
-
/** Lambda runtime. Default:
|
|
204
|
+
/** Lambda runtime. Default: NODEJS_24_X */
|
|
205
205
|
runtime?: Runtime;
|
|
206
206
|
}
|
|
207
207
|
/**
|
|
@@ -317,7 +317,7 @@ export class DevSubstrate extends Construct {
|
|
|
317
317
|
// control plane (ECS / Aurora / SSM), never over the VPC, and the ALB invokes
|
|
318
318
|
// it through the Lambda API — not the network path.
|
|
319
319
|
const waker = new LambdaFunction(this, DEV_SUBSTRATE_WAKER_ID, {
|
|
320
|
-
runtime: Runtime.
|
|
320
|
+
runtime: Runtime.NODEJS_24_X,
|
|
321
321
|
handler: "index.handler",
|
|
322
322
|
code: Code.fromInline(source),
|
|
323
323
|
lambdaDescription: "Dev substrate slot waker",
|
|
@@ -308,7 +308,7 @@ export class Payload extends Construct {
|
|
|
308
308
|
deployment: "code",
|
|
309
309
|
code: Code.fromAsset(`${PAYLOAD_DEFAULTS.SOURCE}/${PAYLOAD_DEFAULTS.OPENNEXT.SERVER}`),
|
|
310
310
|
handler: "index.handler",
|
|
311
|
-
runtime: Runtime.
|
|
311
|
+
runtime: Runtime.NODEJS_24_X,
|
|
312
312
|
architecture: Architecture.ARM_64,
|
|
313
313
|
memorySize,
|
|
314
314
|
timeout,
|
|
@@ -370,7 +370,7 @@ export class Payload extends Construct {
|
|
|
370
370
|
deployment: "code",
|
|
371
371
|
code: Code.fromAsset(`${PAYLOAD_DEFAULTS.SOURCE}/${PAYLOAD_DEFAULTS.OPENNEXT.IMAGE}`),
|
|
372
372
|
handler: "index.handler",
|
|
373
|
-
runtime: Runtime.
|
|
373
|
+
runtime: Runtime.NODEJS_24_X,
|
|
374
374
|
architecture: Architecture.ARM_64,
|
|
375
375
|
memorySize,
|
|
376
376
|
timeout,
|
|
@@ -396,7 +396,7 @@ export class Payload extends Construct {
|
|
|
396
396
|
deployment: "code",
|
|
397
397
|
code: Code.fromAsset(`${PAYLOAD_DEFAULTS.SOURCE}/${PAYLOAD_DEFAULTS.OPENNEXT.REVALIDATION}`),
|
|
398
398
|
handler: "index.handler",
|
|
399
|
-
runtime: Runtime.
|
|
399
|
+
runtime: Runtime.NODEJS_24_X,
|
|
400
400
|
architecture: Architecture.ARM_64,
|
|
401
401
|
memorySize,
|
|
402
402
|
timeout,
|
|
@@ -42,7 +42,7 @@ import { toPascalCase } from "../../utils/capitaliseString.js";
|
|
|
42
42
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
43
43
|
const FORMS_ASSET_DIR = resolve(__dirname, "../../lambda-assets/static-site-forms/asset");
|
|
44
44
|
const FORMS_DEFAULTS = {
|
|
45
|
-
RUNTIME: Runtime.
|
|
45
|
+
RUNTIME: Runtime.NODEJS_24_X,
|
|
46
46
|
ARCHITECTURE: Architecture.ARM_64,
|
|
47
47
|
TIMEOUT_SECONDS: 10,
|
|
48
48
|
MEMORY_MB: 256,
|
|
@@ -4,28 +4,37 @@ import { Duration, Stack, type StackProps } from "aws-cdk-lib";
|
|
|
4
4
|
import { type Role } from "aws-cdk-lib/aws-iam";
|
|
5
5
|
import { AutoScalingGroup, Monitoring, type WarmPoolOptions } from "aws-cdk-lib/aws-autoscaling";
|
|
6
6
|
import { type PersistentDataVolumeProps } from "./persistentDataVolume.js";
|
|
7
|
-
export type Ec2InstancePersistentDataVolumeConfig = Omit<PersistentDataVolumeProps, "autoScalingGroup">;
|
|
7
|
+
export type Ec2InstancePersistentDataVolumeConfig = Omit<PersistentDataVolumeProps, "autoScalingGroup" | "stableOwnerId">;
|
|
8
8
|
/**
|
|
9
9
|
* Caller-supplied ASG `UpdatePolicy` resolution. Default (`undefined`) →
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* `instanceRefresh` with `minHealthyPercentage 0 / maxHealthyPercentage 100`
|
|
11
|
+
* when `persistentDataVolume` is set (single-attach EBS forbids a surge
|
|
12
|
+
* instance), `100 / 200` otherwise (surge-then-shrink, no capacity dip).
|
|
13
|
+
* Either shape propagates userdata/launch-template mutations to running
|
|
14
|
+
* instances on every `LaunchTemplateVersion` change; data continuity for
|
|
15
|
+
* stateful patterns is preserved by `persistentDataVolume`.
|
|
14
16
|
*
|
|
15
17
|
* Variants:
|
|
16
|
-
* - `
|
|
17
|
-
* `
|
|
18
|
+
* - `instanceRefresh` — ASG-native rolling replacement via CFN
|
|
19
|
+
* `UpdatePolicy.InstanceRefresh`; percentages overridable.
|
|
20
|
+
* - `rollingUpdate` — legacy CFN-driven single-instance roll; `pauseTime`
|
|
21
|
+
* overridable. `minInstancesInService` and `maxBatchSize` are fixed at
|
|
22
|
+
* `0` and `1`.
|
|
18
23
|
* - `replacingUpdate` — explicit opt-in to the legacy full-replacement shape.
|
|
19
24
|
* No Fjall consumer uses this today; retained for future bare-EC2 patterns.
|
|
20
|
-
* - `none` — no caller-driven `UpdatePolicy` (omits
|
|
21
|
-
*
|
|
22
|
-
*
|
|
25
|
+
* - `none` — no caller-driven `UpdatePolicy` (omits the rolling, replacing
|
|
26
|
+
* AND refresh blocks). CDK still auto-emits
|
|
27
|
+
* `AutoScalingScheduledAction.IgnoreUnmodifiedGroupSizeProperties`
|
|
23
28
|
* regardless. For callers owning rollout externally.
|
|
24
29
|
*
|
|
25
30
|
* See `aiDocs/troubleshooting/clickhouse-rolling-update-window.md` for the
|
|
26
31
|
* ClickHouse-specific downtime expectations.
|
|
27
32
|
*/
|
|
28
33
|
export type Ec2InstanceUpdatePolicyConfig = {
|
|
34
|
+
type: "instanceRefresh";
|
|
35
|
+
minHealthyPercentage?: number;
|
|
36
|
+
maxHealthyPercentage?: number;
|
|
37
|
+
} | {
|
|
29
38
|
type: "rollingUpdate";
|
|
30
39
|
pauseTime?: Duration;
|
|
31
40
|
} | {
|
|
@@ -105,10 +114,18 @@ export interface Ec2InstanceProps extends StackProps {
|
|
|
105
114
|
*/
|
|
106
115
|
persistentDataVolume?: Ec2InstancePersistentDataVolumeConfig;
|
|
107
116
|
/**
|
|
108
|
-
*
|
|
109
|
-
* `
|
|
110
|
-
*
|
|
111
|
-
*
|
|
117
|
+
* Stable owner identifier for the persistent data volume — becomes the
|
|
118
|
+
* volume's `fjall:OwnerLogicalId` tag and the re-attach Lambdas'
|
|
119
|
+
* `OWNER_LOGICAL_ID`. REQUIRED whenever `persistentDataVolume` is set
|
|
120
|
+
* (rejected at synth otherwise) so the re-attach identity is an explicit
|
|
121
|
+
* caller decision, never an accident of construct-tree position. Must
|
|
122
|
+
* remain byte-stable for the stack's whole life — changing it strands the
|
|
123
|
+
* deployed volume (the Lambda filter stops matching).
|
|
124
|
+
*/
|
|
125
|
+
dataVolumeStableOwnerId?: string;
|
|
126
|
+
/**
|
|
127
|
+
* ASG `UpdatePolicy` resolution. Absent → `UpdatePolicy.instanceRefresh`
|
|
128
|
+
* (0/100 with `persistentDataVolume`, 100/200 otherwise). See
|
|
112
129
|
* `Ec2InstanceUpdatePolicyConfig` for the variant menu and
|
|
113
130
|
* `aiDocs/troubleshooting/clickhouse-rolling-update-window.md` for the
|
|
114
131
|
* downtime-window runbook.
|
|
@@ -3,7 +3,8 @@ import { Construct } from "constructs";
|
|
|
3
3
|
import { resolvePrivateSubnetType } from "../../../utils/vpcUtils.js";
|
|
4
4
|
import { safeEbs } from "./blockDeviceVolume.js";
|
|
5
5
|
import { Duration, Stack, Tags } from "aws-cdk-lib";
|
|
6
|
-
import { AutoScalingGroup, GroupMetrics, Monitoring, TerminationPolicy, UpdatePolicy, OnDemandAllocationStrategy, SpotAllocationStrategy } from "aws-cdk-lib/aws-autoscaling";
|
|
6
|
+
import { AutoScalingGroup, GroupMetrics, InstanceRefreshStrategy, Monitoring, TerminationPolicy, UpdatePolicy, OnDemandAllocationStrategy, SpotAllocationStrategy } from "aws-cdk-lib/aws-autoscaling";
|
|
7
|
+
import { DEFAULT_ROLLING_UPDATE_PAUSE_SECONDS, INSTANCE_REFRESH_DEFAULT_MAX_HEALTHY, INSTANCE_REFRESH_DEFAULT_MIN_HEALTHY, INSTANCE_REFRESH_PDV_MAX_HEALTHY, INSTANCE_REFRESH_PDV_MIN_HEALTHY } from "./ecsConstants.js";
|
|
7
8
|
import { AwsCustomResource } from "../utilities/awsCustomResource.js";
|
|
8
9
|
import { PhysicalResourceId } from "aws-cdk-lib/custom-resources";
|
|
9
10
|
import { Vpc } from "../networking/vpc.js";
|
|
@@ -22,17 +23,17 @@ const DEFAULT_ROOT_DEVICE_NAME = "/dev/xvda";
|
|
|
22
23
|
const DEFAULT_ROOT_VOLUME_SIZE_GIB = 30;
|
|
23
24
|
/**
|
|
24
25
|
* Resolve the caller's `updatePolicy` prop to a CDK `UpdatePolicy`. Default
|
|
25
|
-
* (`config` absent) →
|
|
26
|
-
* `
|
|
27
|
-
* `addAutoScalingGroup` can conditionally spread the
|
|
28
|
-
* `updatePolicy: undefined` directly emits an empty
|
|
29
|
-
* on some CDK versions.
|
|
26
|
+
* (`config` absent) → instance refresh, percentages keyed on
|
|
27
|
+
* `hasPersistentDataVolume` (see the constants above). The `none` branch
|
|
28
|
+
* returns `undefined` so `addAutoScalingGroup` can conditionally spread the
|
|
29
|
+
* field — passing `updatePolicy: undefined` directly emits an empty
|
|
30
|
+
* `UpdatePolicy: {}` block on some CDK versions.
|
|
30
31
|
*
|
|
31
32
|
* Free function rather than a private method: `this` state is not read, and
|
|
32
33
|
* a free function is easier to reach from focused tests.
|
|
33
34
|
*/
|
|
34
|
-
function resolveUpdatePolicy(config) {
|
|
35
|
-
const resolved = config ?? { type: "
|
|
35
|
+
function resolveUpdatePolicy(config, hasPersistentDataVolume) {
|
|
36
|
+
const resolved = config ?? { type: "instanceRefresh" };
|
|
36
37
|
switch (resolved.type) {
|
|
37
38
|
case "none":
|
|
38
39
|
return undefined;
|
|
@@ -42,7 +43,20 @@ function resolveUpdatePolicy(config) {
|
|
|
42
43
|
return UpdatePolicy.rollingUpdate({
|
|
43
44
|
minInstancesInService: 0,
|
|
44
45
|
maxBatchSize: 1,
|
|
45
|
-
pauseTime: resolved.pauseTime ??
|
|
46
|
+
pauseTime: resolved.pauseTime ??
|
|
47
|
+
Duration.seconds(DEFAULT_ROLLING_UPDATE_PAUSE_SECONDS)
|
|
48
|
+
});
|
|
49
|
+
case "instanceRefresh":
|
|
50
|
+
return UpdatePolicy.instanceRefresh({
|
|
51
|
+
strategy: InstanceRefreshStrategy.ROLLING,
|
|
52
|
+
minHealthyPercentage: resolved.minHealthyPercentage ??
|
|
53
|
+
(hasPersistentDataVolume
|
|
54
|
+
? INSTANCE_REFRESH_PDV_MIN_HEALTHY
|
|
55
|
+
: INSTANCE_REFRESH_DEFAULT_MIN_HEALTHY),
|
|
56
|
+
maxHealthyPercentage: resolved.maxHealthyPercentage ??
|
|
57
|
+
(hasPersistentDataVolume
|
|
58
|
+
? INSTANCE_REFRESH_PDV_MAX_HEALTHY
|
|
59
|
+
: INSTANCE_REFRESH_DEFAULT_MAX_HEALTHY)
|
|
46
60
|
});
|
|
47
61
|
default:
|
|
48
62
|
return assertNever(resolved);
|
|
@@ -139,7 +153,7 @@ export class Ec2Instance extends Construct {
|
|
|
139
153
|
? SubnetType.PUBLIC
|
|
140
154
|
: resolvePrivateSubnetType(this.vpc)
|
|
141
155
|
};
|
|
142
|
-
const resolvedUpdatePolicy = resolveUpdatePolicy(props.updatePolicy);
|
|
156
|
+
const resolvedUpdatePolicy = resolveUpdatePolicy(props.updatePolicy, props.persistentDataVolume !== undefined);
|
|
143
157
|
const baseAsgProps = {
|
|
144
158
|
vpc: this.vpc,
|
|
145
159
|
vpcSubnets,
|
|
@@ -242,8 +256,11 @@ export class Ec2Instance extends Construct {
|
|
|
242
256
|
addPersistentDataVolume(props) {
|
|
243
257
|
if (props.persistentDataVolume === undefined)
|
|
244
258
|
return;
|
|
259
|
+
if (props.dataVolumeStableOwnerId === undefined)
|
|
260
|
+
return; // unreachable: validateEc2InstanceProps rejects the pair
|
|
245
261
|
this.persistentDataVolume = new PersistentDataVolume(this, `${props.serviceName}PersistentDataVolume`, {
|
|
246
262
|
autoScalingGroup: this.autoScalingGroup,
|
|
263
|
+
stableOwnerId: props.dataVolumeStableOwnerId,
|
|
247
264
|
...props.persistentDataVolume
|
|
248
265
|
});
|
|
249
266
|
}
|
|
@@ -282,6 +299,13 @@ function validateEc2InstanceProps(props) {
|
|
|
282
299
|
throw new Error(`Ec2Instance.maxCapacity (${props.maxCapacity}) must be >= minCapacity (${props.minCapacity})`);
|
|
283
300
|
}
|
|
284
301
|
if (props.persistentDataVolume !== undefined) {
|
|
302
|
+
if (props.dataVolumeStableOwnerId === undefined ||
|
|
303
|
+
props.dataVolumeStableOwnerId === "") {
|
|
304
|
+
throw new Error("Ec2Instance.persistentDataVolume requires dataVolumeStableOwnerId — " +
|
|
305
|
+
"the volume's re-attach identity must be an explicit, stable " +
|
|
306
|
+
"caller decision (it becomes the fjall:OwnerLogicalId tag the " +
|
|
307
|
+
"lifecycle Lambdas filter on)");
|
|
308
|
+
}
|
|
285
309
|
const azs = props.vpcSubnets?.availabilityZones;
|
|
286
310
|
if (azs === undefined || azs.length !== 1) {
|
|
287
311
|
throw new Error(`Ec2Instance.persistentDataVolume requires vpcSubnets.availabilityZones to be exactly 1 entry; got ${azs?.length ?? 0}`);
|
|
@@ -144,7 +144,7 @@ export class Ec2GracefulTerminationHandler extends Construct {
|
|
|
144
144
|
]
|
|
145
145
|
: [];
|
|
146
146
|
this.lambda = new LambdaFunction(this, `${id}Fn`, {
|
|
147
|
-
runtime: Runtime.
|
|
147
|
+
runtime: Runtime.NODEJS_24_X,
|
|
148
148
|
handler: "index.handler",
|
|
149
149
|
code: Code.fromInline(source),
|
|
150
150
|
lambdaDescription: `${id} ${EC2_GRACEFUL_TERMINATION_HANDLER_DESCRIPTION}`,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type CapacityIdentityPin } from "@fjall/util";
|
|
2
|
+
import type { Ec2CapacityConfig, EcsCapacityProvider } from "./ecsTypes.js";
|
|
3
|
+
/** The service + ec2Config that first claimed a capacity slot. */
|
|
4
|
+
export interface Ec2AsgOrigin {
|
|
5
|
+
serviceName: string;
|
|
6
|
+
ec2Config: Ec2CapacityConfig;
|
|
7
|
+
/** Identity anchor the slot's ASG was built with (pin-frozen or slot-derived). */
|
|
8
|
+
anchor: string;
|
|
9
|
+
}
|
|
10
|
+
/** The slot this ec2Config binds to (`"primary"` when undeclared). */
|
|
11
|
+
export declare function resolveCapacitySlot(ec2Config: Ec2CapacityConfig): string;
|
|
12
|
+
/** Rejects at synth a slot name outside the kebab grammar. */
|
|
13
|
+
export declare function assertValidCapacitySlot(slot: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* The identity anchor every CloudFormation identity surface derives from.
|
|
16
|
+
* Precedence: explicit `identityAnchor` config pin, then the deploy-injected
|
|
17
|
+
* context pin's `legacyAnchor`, then `toPascalCase(slot)`. The anchor grammar
|
|
18
|
+
* is validated whichever source wins — a bad anchor corrupts every logical
|
|
19
|
+
* ID and physical name downstream.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveCapacityAnchor(ec2Config: Ec2CapacityConfig, pin?: CapacityIdentityPin): string;
|
|
22
|
+
/**
|
|
23
|
+
* Rejects at synth two slots whose anchors collide (`web-1` and `web1` both
|
|
24
|
+
* normalise to `Web1`) — colliding anchors mean colliding logical IDs.
|
|
25
|
+
* `anchorsBySlot` carries every anchor already resolved for the cluster.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertAnchorUnique(slot: string, anchor: string, anchorsBySlot: ReadonlyMap<string, string>): void;
|
|
28
|
+
/**
|
|
29
|
+
* Rejects at synth when a service reusing a shared ASG is incompatible with
|
|
30
|
+
* it: the slot's identity is contested (`identityAnchor` disagreement), the
|
|
31
|
+
* origin's `persistentDataVolume` makes the slot a singleton (two tasks
|
|
32
|
+
* would race to attach the same EBS volume), or the reusing service asks for
|
|
33
|
+
* ASG settings that differ from the ones the ASG was built with — those
|
|
34
|
+
* settings would be silently ignored (same silent-ignore class as
|
|
35
|
+
* `validateEc2ServiceSizing`).
|
|
36
|
+
*/
|
|
37
|
+
export declare function assertSharedAsgConfigMatches(serviceName: string, ec2Config: Ec2CapacityConfig, slot: string, origin: Ec2AsgOrigin): void;
|
|
38
|
+
/**
|
|
39
|
+
* Config fields that fed the pre-6.0 (config-keyed) identity derivation —
|
|
40
|
+
* the "identity derives from …" causes named in manifest identity aliases.
|
|
41
|
+
*/
|
|
42
|
+
export declare const LEGACY_KEY_FIELDS: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* The pre-6.0 config-derived identity key. SURVIVES ONLY as the legacy
|
|
45
|
+
* checksum behind manifest identity-alias hints (`legacyKey`/`legacyAnchor`)
|
|
46
|
+
* — it feeds no construct IDs, no physical names and no template properties.
|
|
47
|
+
* ASG sharing keys on the capacity SLOT.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getEc2ConfigKey(ec2Config: Ec2CapacityConfig): string;
|
|
50
|
+
/**
|
|
51
|
+
* The anchor a pre-6.0 synth derived from a config key — the exact `safeKey`
|
|
52
|
+
* strip the factory used (`key.replace(/[^a-zA-Z0-9]/g, "")`). Feeds the
|
|
53
|
+
* manifest alias hints and pin capture; never a live identity surface.
|
|
54
|
+
*/
|
|
55
|
+
export declare function legacyAnchorFromKey(key: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Minimal structural input for {@link validateSharedEc2CapacityConfig} —
|
|
58
|
+
* exactly the fields the check reads. Both the resources-layer
|
|
59
|
+
* `EcsServiceProps` and the patterns-layer `EcsServiceConfig` structurally
|
|
60
|
+
* satisfy it (typescript-standards § "Helpers consume resolved state, not the
|
|
61
|
+
* dispatch enum").
|
|
62
|
+
*/
|
|
63
|
+
export interface SharedEc2CapacityServiceInput {
|
|
64
|
+
name: string;
|
|
65
|
+
capacityProvider?: EcsCapacityProvider;
|
|
66
|
+
ec2Config?: Ec2CapacityConfig;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Cross-service capacity-slot compatibility: validates slot grammar, rejects
|
|
70
|
+
* anchor collisions across slots, and rejects a slot reuse that is
|
|
71
|
+
* incompatible with the slot's origin — config drift the shared ASG would
|
|
72
|
+
* silently discard, a contested identity, or a `persistentDataVolume`
|
|
73
|
+
* singleton being shared. Pure; called from BOTH validation layers —
|
|
74
|
+
* resources `validateEcsClusterProps` and patterns `validateEcsProps`
|
|
75
|
+
* (generator-standards § "Validate at the Lowest Layer the Field Belongs To")
|
|
76
|
+
* — with the same checks at the construction site
|
|
77
|
+
* (`getOrCreateAsgCapacityProvider`) as defence-in-depth.
|
|
78
|
+
*
|
|
79
|
+
* Anchor collisions are checked on the PIN-BLIND anchor (`identityAnchor` or
|
|
80
|
+
* slot derivation) — this layer is pure and cannot read the deploy-injected
|
|
81
|
+
* context pins. The factory re-checks with pins applied.
|
|
82
|
+
*/
|
|
83
|
+
export declare function validateSharedEc2CapacityConfig(services: readonly SharedEc2CapacityServiceInput[]): void;
|