@fjall/components-infrastructure 2.16.0 → 2.18.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/lambda-assets/cert-generator/asset/index.js +14 -1
- package/dist/lib/patterns/aws/clickhouseDatabase.d.ts +17 -0
- package/dist/lib/patterns/aws/clickhouseDatabase.js +16 -0
- package/dist/lib/patterns/aws/compute.d.ts +2 -2
- package/dist/lib/patterns/aws/computeEcs.d.ts +12 -1
- package/dist/lib/patterns/aws/computeEcs.js +57 -0
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +51 -1
- package/dist/lib/patterns/aws/database.d.ts +19 -1
- package/dist/lib/patterns/aws/database.js +21 -2
- package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.js +13 -3
- package/dist/lib/resources/aws/compute/ecs.js +14 -3
- package/dist/lib/resources/aws/compute/ecsConstants.d.ts +2 -0
- package/dist/lib/resources/aws/compute/ecsConstants.js +4 -0
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.d.ts +2 -0
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +13 -4
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +17 -1
- package/dist/lib/resources/aws/compute/ecsValidation.d.ts +7 -0
- package/dist/lib/resources/aws/compute/ecsValidation.js +10 -0
- package/dist/lib/resources/aws/compute/lambda.js +20 -2
- package/dist/lib/resources/aws/compute/persistentDataVolume.js +5 -1
- package/dist/lib/resources/aws/database/rdsInstance.d.ts +19 -0
- package/dist/lib/resources/aws/database/rdsInstance.js +13 -1
- package/dist/lib/resources/aws/messaging/sns.d.ts +5 -0
- package/dist/lib/resources/aws/messaging/sns.js +7 -1
- package/dist/lib/resources/aws/messaging/sqs.d.ts +6 -0
- package/dist/lib/resources/aws/messaging/sqs.js +10 -2
- package/dist/lib/resources/aws/monitoring/clickhouseAlarms.d.ts +10 -15
- package/dist/lib/resources/aws/monitoring/clickhouseAlarms.js +34 -56
- package/dist/lib/resources/aws/monitoring/ecsAlarms.js +5 -1
- package/dist/lib/resources/aws/monitoring/index.d.ts +2 -0
- package/dist/lib/resources/aws/monitoring/index.js +2 -0
- package/dist/lib/resources/aws/monitoring/logPatternAlarms.d.ts +55 -0
- package/dist/lib/resources/aws/monitoring/logPatternAlarms.js +74 -0
- package/dist/lib/resources/aws/monitoring/metricNamespaces.d.ts +13 -0
- package/dist/lib/resources/aws/monitoring/metricNamespaces.js +12 -0
- package/package.json +5 -7
|
@@ -17897,6 +17897,15 @@ ${body}
|
|
|
17897
17897
|
-----END PRIVATE KEY-----
|
|
17898
17898
|
`;
|
|
17899
17899
|
}
|
|
17900
|
+
function certPhysicalResourceId(event, caSecretArn, serverSecretArn) {
|
|
17901
|
+
if (event.RequestType === "Update" && event.PhysicalResourceId) {
|
|
17902
|
+
return event.PhysicalResourceId;
|
|
17903
|
+
}
|
|
17904
|
+
const digest = createHash("sha256").update(`${caSecretArn}
|
|
17905
|
+
${serverSecretArn}`).digest("hex").slice(0, 16);
|
|
17906
|
+
return `tls-cert-${digest}`;
|
|
17907
|
+
}
|
|
17908
|
+
exports.certPhysicalResourceId = certPhysicalResourceId;
|
|
17900
17909
|
exports.handler = async (event) => {
|
|
17901
17910
|
if (event.RequestType === "Delete") {
|
|
17902
17911
|
const props2 = event.ResourceProperties || {};
|
|
@@ -17964,7 +17973,11 @@ exports.handler = async (event) => {
|
|
|
17964
17973
|
throw err;
|
|
17965
17974
|
}
|
|
17966
17975
|
return {
|
|
17967
|
-
PhysicalResourceId:
|
|
17976
|
+
PhysicalResourceId: certPhysicalResourceId(
|
|
17977
|
+
event,
|
|
17978
|
+
caSecretArn,
|
|
17979
|
+
serverSecretArn
|
|
17980
|
+
),
|
|
17968
17981
|
Data: { CaCertSha256: caCertSha256 }
|
|
17969
17982
|
};
|
|
17970
17983
|
};
|
|
@@ -5,6 +5,7 @@ import { Construct } from "constructs";
|
|
|
5
5
|
import { Secret, type SecretImport } from "../../resources/aws/secrets/secret.js";
|
|
6
6
|
import { type ClickHouseSchemaAdmin, type ProfileSpec } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
7
7
|
import { type ISecret } from "aws-cdk-lib/aws-secretsmanager";
|
|
8
|
+
import { type ITopic } from "aws-cdk-lib/aws-sns";
|
|
8
9
|
import type { ClickHouseTlsOptions } from "./clickhouseTls/index.js";
|
|
9
10
|
import { type ClickHouseMigrationsConfig, type IClickHouseDatabase } from "./interfaces/database.js";
|
|
10
11
|
import { type ISecurityGroupConnector } from "./interfaces/connector.js";
|
|
@@ -115,6 +116,22 @@ export interface ClickHouseDatabaseProps {
|
|
|
115
116
|
* See ./clickhouseTls/types.ts.
|
|
116
117
|
*/
|
|
117
118
|
tls?: ClickHouseTlsOptions;
|
|
119
|
+
/**
|
|
120
|
+
* Ops alarm SNS topic for the ClickHouse host-posture alarms (CPU / memory /
|
|
121
|
+
* disk warn+critical) and — when `backupSchedule` is enabled — the
|
|
122
|
+
* backup-failure alarm. Accepts an `ITopic`, an `arn:` string, or the
|
|
123
|
+
* `"import:<ExportName>"` form the generator scaffolds onto production apps
|
|
124
|
+
* (`"import:SharedAlarmTopicArn"`). Forwarded automatically by
|
|
125
|
+
* `DatabaseFactory.build` from `BaseDatabaseProps.alertsTopic`. Omitted (the
|
|
126
|
+
* default) → no host alarms, matching the dormant pre-dogfood behaviour.
|
|
127
|
+
*
|
|
128
|
+
* Resolved internally via `resolveAlertsTopic`; the construct owns the
|
|
129
|
+
* `instanceRole` / `asgName` / `backupTaskLogGroup` the alarms need, so no
|
|
130
|
+
* caller wiring is required. The stuck-merge alarm is NOT declared here —
|
|
131
|
+
* `"Stuck merge detected"` is emitted by the webapp app process, so it lives
|
|
132
|
+
* on the app service's declarative `logAlarms` instead.
|
|
133
|
+
*/
|
|
134
|
+
alertsTopic?: ITopic | string;
|
|
118
135
|
}
|
|
119
136
|
/**
|
|
120
137
|
* ClickHouse analytics database wrapper implementing IClickHouseDatabase.
|
|
@@ -18,6 +18,8 @@ import { LogGroup } from "../../resources/aws/logging/logGroup.js";
|
|
|
18
18
|
import { createClickHouseSecurityGroup } from "../../resources/aws/database/clickhouseSecurityGroup.js";
|
|
19
19
|
import { buildClickHouseEntrypointWrapper, buildClickHouseUserData, generateUsersConfigXml } from "../../resources/aws/database/clickhouseUserData.js";
|
|
20
20
|
import { toPascalCase } from "../../utils/capitaliseString.js";
|
|
21
|
+
import { resolveAlertsTopic } from "../../utils/resolveAlertsTopic.js";
|
|
22
|
+
import { createClickHouseAlarms } from "../../resources/aws/monitoring/index.js";
|
|
21
23
|
import { ClickHouseSchemaAdminSchema, ManagedPasswordNameSchema, ProfileSpecSchema, ClickHouseDefaultProfiles, PROFILE_NAME_PATTERN } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
22
24
|
import { inferAmiHardwareType } from "../../resources/aws/compute/ecsConstants.js";
|
|
23
25
|
import { CLICKHOUSE_DATABASE_NAME, DEFAULT_CLICKHOUSE_INSTANCE_TYPE, CLICKHOUSE_IMAGE, CLICKHOUSE_EBS_VOLUME_SIZE_GB, CLICKHOUSE_EBS_IOPS, CLICKHOUSE_EBS_THROUGHPUT_MBPS, CLICKHOUSE_TASK_MEMORY_MIB, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_HTTPS_PORT, CLICKHOUSE_NATIVE_PORT, CLICKHOUSE_TCP_SECURE_PORT, CLICKHOUSE_TLS_CERT_MOUNT_PATH, CLICKHOUSE_PROMETHEUS_PORT, CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_SECRET_OPTIONS, CLICKHOUSE_SERVER_ROLE_TAG, clickHouseUserSecretName, CLICKHOUSE_HEALTH_CHECK, CLICKHOUSE_STOP_TIMEOUT_SECONDS, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, userPasswordEnvName, OPTIMISE_FINAL_SCHEDULE, REPLACING_MERGE_TREE_TABLES, OPTIMISE_MV_TABLES, CLICKHOUSE_CLOUDMAP_SERVICE_NAME, CLICKHOUSE_SERVER_CONTAINER_NAME, OPTIMISE_TASK_MEMORY_MIB, OPTIMISE_TASK_CPU_UNITS, BACKUP_SCHEDULE, BACKUP_TASK_MEMORY_MIB, BACKUP_TASK_CPU_UNITS, BACKUP_RETENTION_DAYS } from "../../resources/aws/database/clickhouseConstants.js";
|
|
@@ -480,6 +482,20 @@ export class ClickHouseDatabase extends Construct {
|
|
|
480
482
|
tlsCaSecret.grantRead(instanceRole);
|
|
481
483
|
if (tlsServerSecret !== undefined)
|
|
482
484
|
tlsServerSecret.grantRead(instanceRole);
|
|
485
|
+
// asgName is the CloudWatch dimension the host metrics key off, so without
|
|
486
|
+
// it the alarms cannot be built; no topic is the default, so skip silently
|
|
487
|
+
// rather than throw.
|
|
488
|
+
const resolvedAlertsTopic = resolveAlertsTopic(this, "AlertsTopic", props.alertsTopic);
|
|
489
|
+
const asgName = ecsCompute.getAutoScalingGroupName();
|
|
490
|
+
if (resolvedAlertsTopic !== undefined && asgName !== undefined) {
|
|
491
|
+
createClickHouseAlarms({
|
|
492
|
+
scope: this,
|
|
493
|
+
instanceRole,
|
|
494
|
+
asgName,
|
|
495
|
+
alarmTopic: resolvedAlertsTopic,
|
|
496
|
+
...(backupTaskLogGroup !== undefined && { backupTaskLogGroup })
|
|
497
|
+
});
|
|
498
|
+
}
|
|
483
499
|
const adminSecretName = clickHouseUserSecretName(schemaAdmin.name);
|
|
484
500
|
// Password via CLICKHOUSE_CLIENT_PASSWORD env, not --password on argv
|
|
485
501
|
// (argv → /proc/<pid>/cmdline). `jq -r .password` on an empty pipeline
|
|
@@ -3,10 +3,10 @@ import { type Construct } from "constructs";
|
|
|
3
3
|
import { type SecretImport } from "../../resources/aws/secrets/index.js";
|
|
4
4
|
import { type ComputeType, type IEcsCompute, type ILambdaCompute, type IEc2Compute, type AnyCompute, isCompute, isEcsCompute, isLambdaCompute, isEc2Compute } from "./interfaces/compute.js";
|
|
5
5
|
import type App from "../../app.js";
|
|
6
|
-
import { EcsCompute, type EcsComputeProps, type EcsServiceConfig, type EcsContainerConfig, type EcsScalingConfig, type EcsClusterConfig, type EcsRoutingConfig, type EcsCapacityProviderConfig, type ContainerDependency, type EcsMigrationsConfig, ECS_CAPACITY_PROVIDER_CONFIG, getEcsCapacityProviderConfig, ScalingType, type EcsCapacityProvider, type Ec2CapacityConfig, validateEcsProps, buildContainerConfigs, expandMigrationsSugar, type ResolvedScalingConfig, resolveScalingConfig } from "./computeEcs.js";
|
|
6
|
+
import { EcsCompute, type EcsComputeProps, type EcsServiceConfig, type ServiceLogAlarm, type EcsContainerConfig, type EcsScalingConfig, type EcsClusterConfig, type EcsRoutingConfig, type EcsCapacityProviderConfig, type ContainerDependency, type EcsMigrationsConfig, ECS_CAPACITY_PROVIDER_CONFIG, getEcsCapacityProviderConfig, ScalingType, type EcsCapacityProvider, type Ec2CapacityConfig, validateEcsProps, buildContainerConfigs, expandMigrationsSugar, type ResolvedScalingConfig, resolveScalingConfig } from "./computeEcs.js";
|
|
7
7
|
import { LambdaCompute, type LambdaComputeProps, type ContainerLambdaProps, type CodeLambdaProps, type FunctionUrlConfig, type ResolvedLambdaDeployment, resolveLambdaDeployment, Architecture, HttpMethod, InvokeMode, type FunctionUrlCorsOptions } from "./computeLambda.js";
|
|
8
8
|
import { Ec2Compute, type Ec2ComputeProps, type SshConfig } from "./computeEc2.js";
|
|
9
|
-
export { EcsCompute, type EcsComputeProps, type EcsServiceConfig, type EcsContainerConfig, type EcsScalingConfig, type EcsClusterConfig, type EcsRoutingConfig, type EcsCapacityProviderConfig, ECS_CAPACITY_PROVIDER_CONFIG, getEcsCapacityProviderConfig, ScalingType, type EcsCapacityProvider, type Ec2CapacityConfig, type ContainerDependency, type EcsMigrationsConfig, validateEcsProps, buildContainerConfigs, expandMigrationsSugar, type ResolvedScalingConfig, resolveScalingConfig, LambdaCompute, type LambdaComputeProps, type ContainerLambdaProps, type CodeLambdaProps, type FunctionUrlConfig, type ResolvedLambdaDeployment, resolveLambdaDeployment, Architecture, HttpMethod, InvokeMode, type FunctionUrlCorsOptions, Ec2Compute, type Ec2ComputeProps, type SshConfig };
|
|
9
|
+
export { EcsCompute, type EcsComputeProps, type EcsServiceConfig, type ServiceLogAlarm, type EcsContainerConfig, type EcsScalingConfig, type EcsClusterConfig, type EcsRoutingConfig, type EcsCapacityProviderConfig, ECS_CAPACITY_PROVIDER_CONFIG, getEcsCapacityProviderConfig, ScalingType, type EcsCapacityProvider, type Ec2CapacityConfig, type ContainerDependency, type EcsMigrationsConfig, validateEcsProps, buildContainerConfigs, expandMigrationsSugar, type ResolvedScalingConfig, resolveScalingConfig, LambdaCompute, type LambdaComputeProps, type ContainerLambdaProps, type CodeLambdaProps, type FunctionUrlConfig, type ResolvedLambdaDeployment, resolveLambdaDeployment, Architecture, HttpMethod, InvokeMode, type FunctionUrlCorsOptions, Ec2Compute, type Ec2ComputeProps, type SshConfig };
|
|
10
10
|
export type { ComputeType } from "./interfaces/compute.js";
|
|
11
11
|
/**
|
|
12
12
|
* Configuration defaults for each compute type.
|
|
@@ -7,7 +7,7 @@ import { type IEcsCompute } from "./interfaces/compute.js";
|
|
|
7
7
|
import { type SecretImport } from "../../resources/aws/secrets/index.js";
|
|
8
8
|
import EcsCluster, { type EcsClusterProps } from "../../resources/aws/compute/ecs.js";
|
|
9
9
|
export { ScalingType } from "./computeEcsTypes.js";
|
|
10
|
-
export type { EcsCapacityProvider, Ec2CapacityConfig, RemoteConnectionSpec, EcsCapacityProviderConfig, EcsContainerConfig, ContainerDependency, ContainerVolume, EcsScheduledTaskConfig, EcsLifecycleHookMigrationsConfig, EcsPostDeployMigrationsConfig, EcsHookMigrationsConfig, EcsMigrationsConfig, EcsMigrationsMode, EcsCircuitBreakerConfig, EcsScalingConfig, EcsClusterConfig, EcsRoutingConfig, EcsServiceConfig, EcsComputeProps } from "./computeEcsTypes.js";
|
|
10
|
+
export type { EcsCapacityProvider, Ec2CapacityConfig, RemoteConnectionSpec, EcsCapacityProviderConfig, EcsContainerConfig, ContainerDependency, ContainerVolume, EcsScheduledTaskConfig, EcsLifecycleHookMigrationsConfig, EcsPostDeployMigrationsConfig, EcsHookMigrationsConfig, EcsMigrationsConfig, EcsMigrationsMode, EcsCircuitBreakerConfig, EcsScalingConfig, EcsClusterConfig, EcsRoutingConfig, EcsServiceConfig, ServiceLogAlarm, EcsComputeProps } from "./computeEcsTypes.js";
|
|
11
11
|
import { ScalingType, type EcsCapacityProviderConfig, type EcsCapacityProvider, type EcsContainerConfig, type EcsScalingConfig, type EcsServiceConfig, type EcsComputeProps, type QueueScalingConfig } from "./computeEcsTypes.js";
|
|
12
12
|
export declare const ECS_CAPACITY_PROVIDER_CONFIG: Record<EcsCapacityProvider, EcsCapacityProviderConfig>;
|
|
13
13
|
export declare function getEcsCapacityProviderConfig(provider: EcsCapacityProvider): EcsCapacityProviderConfig;
|
|
@@ -124,6 +124,17 @@ export declare class EcsCompute extends Construct implements IEcsCompute {
|
|
|
124
124
|
* migrate container injected by `expandMigrationsSugar`.
|
|
125
125
|
*/
|
|
126
126
|
private wireLifecycleHookMigrations;
|
|
127
|
+
/**
|
|
128
|
+
* Wire a native CloudFormation `DependsOn` from each service that declares
|
|
129
|
+
* `awaitMigrationsFrom` to the named migrate-owning service, so CFN does not
|
|
130
|
+
* roll out the dependent service until the target's ECS deployment — including
|
|
131
|
+
* its PRE_SCALE_UP migrate hook — has completed. Prevents the RDS-IAM auth /
|
|
132
|
+
* schema-mismatch race when a DB-connecting worker starts before the app's
|
|
133
|
+
* migrations have applied, and enables a single-pass greenfield deploy.
|
|
134
|
+
* Shape (target exists, declares hook migrations, no self / cycle) is already
|
|
135
|
+
* guaranteed by `validateEcsProps`; the undefined guards are synth invariants.
|
|
136
|
+
*/
|
|
137
|
+
private wireServiceMigrationDependencies;
|
|
127
138
|
/**
|
|
128
139
|
* Synthesise a dedicated migration task definition for a lifecycle-hook
|
|
129
140
|
* migration when `separateTaskDef` is set. Creates the migration's own
|
|
@@ -154,6 +154,39 @@ export function validateEcsProps(props) {
|
|
|
154
154
|
validateSeparateTaskDef(service.name, service.migrations.separateTaskDef);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
// Validate awaitMigrationsFrom cross-service migration ordering.
|
|
158
|
+
const serviceByName = new Map(props.services.map((s) => [s.name, s]));
|
|
159
|
+
for (const service of props.services) {
|
|
160
|
+
const target = service.awaitMigrationsFrom;
|
|
161
|
+
if (target === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
if (target === service.name) {
|
|
164
|
+
throw new Error(`Service '${service.name}': awaitMigrationsFrom cannot reference its own migrations.`);
|
|
165
|
+
}
|
|
166
|
+
const targetService = serviceByName.get(target);
|
|
167
|
+
if (targetService === undefined) {
|
|
168
|
+
throw new Error(`Service '${service.name}': awaitMigrationsFrom names unknown service '${target}'. ` +
|
|
169
|
+
"It must name another service in the same cluster.");
|
|
170
|
+
}
|
|
171
|
+
if (targetService.migrations === undefined ||
|
|
172
|
+
!isHookMigrations(targetService.migrations)) {
|
|
173
|
+
throw new Error(`Service '${service.name}': awaitMigrationsFrom names service '${target}', which does not ` +
|
|
174
|
+
"declare lifecycle-hook or post-deploy migrations. Only those modes have a single " +
|
|
175
|
+
"completion point to await (init-container migrations run per-replica).");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Detect cycles in the awaitMigrationsFrom graph (each service awaits <=1 other).
|
|
179
|
+
for (const service of props.services) {
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
let current = service.name;
|
|
182
|
+
while (current !== undefined) {
|
|
183
|
+
if (seen.has(current)) {
|
|
184
|
+
throw new Error(`Service '${service.name}': awaitMigrationsFrom forms a dependency cycle.`);
|
|
185
|
+
}
|
|
186
|
+
seen.add(current);
|
|
187
|
+
current = serviceByName.get(current)?.awaitMigrationsFrom;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
157
190
|
if (props.cluster?.directAccess === true) {
|
|
158
191
|
const hasEc2Service = props.services.some((s) => s.capacityProvider === "EC2");
|
|
159
192
|
if (!hasEc2Service) {
|
|
@@ -722,6 +755,8 @@ export class EcsCompute extends Construct {
|
|
|
722
755
|
ssmSecretsPath: service.ssmSecretsPath,
|
|
723
756
|
docker: service.docker,
|
|
724
757
|
alarms: service.alarms,
|
|
758
|
+
logAlarms: service.logAlarms,
|
|
759
|
+
logMetricNamespace: service.logMetricNamespace,
|
|
725
760
|
circuitBreaker: service.circuitBreaker,
|
|
726
761
|
...(service.deployment !== undefined && {
|
|
727
762
|
deployment: service.deployment
|
|
@@ -763,6 +798,7 @@ export class EcsCompute extends Construct {
|
|
|
763
798
|
this.ecsCluster = new EcsCluster(this, `${id}Ecs`, ecsProps);
|
|
764
799
|
this.connections = this.ecsCluster.connections;
|
|
765
800
|
this.wireLifecycleHookMigrations(props.services);
|
|
801
|
+
this.wireServiceMigrationDependencies(props.services);
|
|
766
802
|
this.materialiseScheduledTasks(id, props);
|
|
767
803
|
}
|
|
768
804
|
/**
|
|
@@ -985,6 +1021,27 @@ export class EcsCompute extends Construct {
|
|
|
985
1021
|
});
|
|
986
1022
|
}
|
|
987
1023
|
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Wire a native CloudFormation `DependsOn` from each service that declares
|
|
1026
|
+
* `awaitMigrationsFrom` to the named migrate-owning service, so CFN does not
|
|
1027
|
+
* roll out the dependent service until the target's ECS deployment — including
|
|
1028
|
+
* its PRE_SCALE_UP migrate hook — has completed. Prevents the RDS-IAM auth /
|
|
1029
|
+
* schema-mismatch race when a DB-connecting worker starts before the app's
|
|
1030
|
+
* migrations have applied, and enables a single-pass greenfield deploy.
|
|
1031
|
+
* Shape (target exists, declares hook migrations, no self / cycle) is already
|
|
1032
|
+
* guaranteed by `validateEcsProps`; the undefined guards are synth invariants.
|
|
1033
|
+
*/
|
|
1034
|
+
wireServiceMigrationDependencies(services) {
|
|
1035
|
+
for (const svcConfig of services) {
|
|
1036
|
+
if (svcConfig.awaitMigrationsFrom === undefined)
|
|
1037
|
+
continue;
|
|
1038
|
+
const dependent = this.ecsCluster.getService(svcConfig.name);
|
|
1039
|
+
const dependency = this.ecsCluster.getService(svcConfig.awaitMigrationsFrom);
|
|
1040
|
+
if (dependent !== undefined && dependency !== undefined) {
|
|
1041
|
+
dependent.node.addDependency(dependency);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
988
1045
|
/**
|
|
989
1046
|
* Synthesise a dedicated migration task definition for a lifecycle-hook
|
|
990
1047
|
* migration when `separateTaskDef` is set. Creates the migration's own
|
|
@@ -8,7 +8,7 @@ import { type ConnectionSpec } from "./interfaces/connector.js";
|
|
|
8
8
|
import { type RemoteConnectionSpec } from "../../resources/aws/compute/ecsRemoteConnections.js";
|
|
9
9
|
import { type EcsRoutingConfig, type EcsContainerDependency } from "../../resources/aws/compute/ecsTypes.js";
|
|
10
10
|
import { ScalingType, type DomainConfig, type EcsCapacityProvider, type Ec2CapacityConfig, type QueueScalingConfig } from "../../resources/aws/compute/ecs.js";
|
|
11
|
-
import type { EcsServiceAlarmThresholds } from "../../resources/aws/monitoring/index.js";
|
|
11
|
+
import type { EcsServiceAlarmThresholds, LogPatternAlarmSpec } from "../../resources/aws/monitoring/index.js";
|
|
12
12
|
import { type SecretImport } from "../../resources/aws/secrets/index.js";
|
|
13
13
|
import type { DockerBuild } from "@fjall/util/manifest/schemas";
|
|
14
14
|
export type { RemoteConnectionSpec };
|
|
@@ -39,6 +39,13 @@ export interface EcsCapacityProviderConfig {
|
|
|
39
39
|
* @see https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDependency.html
|
|
40
40
|
*/
|
|
41
41
|
export type ContainerDependency = EcsContainerDependency;
|
|
42
|
+
/**
|
|
43
|
+
* A log-pattern alarm declared on a service: a CloudWatch metric filter over the
|
|
44
|
+
* service's log group plus the alarm that fires on a match. Public-facing alias
|
|
45
|
+
* for the canonical resource-layer `LogPatternAlarmSpec`, re-exported so factory
|
|
46
|
+
* consumers can declare `logAlarms` from the patterns barrel.
|
|
47
|
+
*/
|
|
48
|
+
export type ServiceLogAlarm = LogPatternAlarmSpec;
|
|
42
49
|
/**
|
|
43
50
|
* Configuration for a container in an ECS task.
|
|
44
51
|
*
|
|
@@ -729,6 +736,20 @@ export interface EcsServiceConfig {
|
|
|
729
736
|
* - object: override specific thresholds
|
|
730
737
|
*/
|
|
731
738
|
alarms?: EcsServiceAlarmThresholds | false;
|
|
739
|
+
/**
|
|
740
|
+
* Log-pattern alarms for this service. Each entry creates a CloudWatch metric
|
|
741
|
+
* filter over the service's log group plus an alarm wired to the cluster's
|
|
742
|
+
* `alertsTopic` — so it materialises only when `alertsTopic` is set and
|
|
743
|
+
* `alarms !== false`. Declarative: describe the match with `literal` or
|
|
744
|
+
* `anyTerms`.
|
|
745
|
+
*/
|
|
746
|
+
logAlarms?: ServiceLogAlarm[];
|
|
747
|
+
/**
|
|
748
|
+
* Default CloudWatch metric namespace for this service's `logAlarms`. Each
|
|
749
|
+
* entry may override it per-alarm (e.g. `Fjall/WebApp` for app alarms,
|
|
750
|
+
* `Fjall/ClickHouse` for a relocated stuck-merge alarm on the same service).
|
|
751
|
+
*/
|
|
752
|
+
logMetricNamespace?: string;
|
|
732
753
|
/**
|
|
733
754
|
* Run an init container before any other container in this service starts.
|
|
734
755
|
* Synthesises a non-essential container with the given migration command,
|
|
@@ -739,6 +760,35 @@ export interface EcsServiceConfig {
|
|
|
739
760
|
* migrations: { command: ["npx", "payload", "migrate"] }
|
|
740
761
|
*/
|
|
741
762
|
migrations?: EcsMigrationsConfig;
|
|
763
|
+
/**
|
|
764
|
+
* Names another service in the same cluster whose **lifecycle-hook** (or
|
|
765
|
+
* post-deploy) migrations must finish before this service rolls out.
|
|
766
|
+
*
|
|
767
|
+
* Implemented as a native CloudFormation `DependsOn`: CFN will not begin
|
|
768
|
+
* creating or updating this service until the named service's ECS deployment
|
|
769
|
+
* — including its PRE_SCALE_UP migrate hook — has completed. Use it for
|
|
770
|
+
* DB-connecting workers that must not start until the migrate-owning service
|
|
771
|
+
* has applied role / grant / schema migrations; without it, a worker can
|
|
772
|
+
* attempt RDS-IAM auth before the migrate task has run `GRANT rds_iam` (the
|
|
773
|
+
* race that aborts a migration-bearing deploy). Also enables a single-pass
|
|
774
|
+
* greenfield deploy (app migrates, then workers come up, in one `cdk deploy`).
|
|
775
|
+
*
|
|
776
|
+
* Works for EC2-capacity services — it is a resource-ordering edge, not a
|
|
777
|
+
* worker-side lifecycle hook, so the FARGATE-only restriction on hook
|
|
778
|
+
* migrations does not apply here.
|
|
779
|
+
*
|
|
780
|
+
* The named service MUST declare `migrations` in `lifecycle-hook` or
|
|
781
|
+
* `post-deploy` mode — init-container migrations run per-replica and have no
|
|
782
|
+
* single completion point to await. Validated at synth (target exists,
|
|
783
|
+
* declares hook migrations, no self-reference, no cycle).
|
|
784
|
+
*
|
|
785
|
+
* Escape hatch for orderings this field can't express: drop to the L2 API,
|
|
786
|
+
* `cluster.getService("x")?.node.addDependency(cluster.getService("y")!)`.
|
|
787
|
+
*
|
|
788
|
+
* @example
|
|
789
|
+
* awaitMigrationsFrom: "app"
|
|
790
|
+
*/
|
|
791
|
+
awaitMigrationsFrom?: string;
|
|
742
792
|
/**
|
|
743
793
|
* Deployment circuit breaker policy. Omit for the safe default
|
|
744
794
|
* `{ enable: true, rollback: true }` — failed deployments automatically
|
|
@@ -131,6 +131,14 @@ export interface InstanceDatabaseProps extends BaseDatabaseProps {
|
|
|
131
131
|
credentials?: CredentialsConfig;
|
|
132
132
|
encryption?: EncryptionConfig;
|
|
133
133
|
publiclyAccessible?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Enable RDS IAM database authentication (opt-in; default off). Instance
|
|
136
|
+
* databases only. When true, IAM principals granted via
|
|
137
|
+
* {@link RelationalDatabase.grantIamConnect} connect with short-lived
|
|
138
|
+
* `rds-db:connect` tokens instead of a stored password. See ADR
|
|
139
|
+
* decisions/2026-06-17-rls-role-auth-and-launch-gating.md.
|
|
140
|
+
*/
|
|
141
|
+
iamAuthentication?: boolean;
|
|
134
142
|
/** ARN or identifier of DB instance snapshot to restore from */
|
|
135
143
|
snapshotIdentifier?: string;
|
|
136
144
|
/** Username from the snapshot (required when restoring from snapshot to reset password) */
|
|
@@ -247,7 +255,7 @@ export declare class RelationalDatabase extends Construct implements IRelational
|
|
|
247
255
|
private database;
|
|
248
256
|
constructor(scope: Construct, id: string, props: IRelationalDatabaseProps);
|
|
249
257
|
private resolveAlertsTopic;
|
|
250
|
-
addDatabase
|
|
258
|
+
private addDatabase;
|
|
251
259
|
private addAurora;
|
|
252
260
|
private addAuroraGlobal;
|
|
253
261
|
private addRdsInstance;
|
|
@@ -278,6 +286,16 @@ export declare class RelationalDatabase extends Construct implements IRelational
|
|
|
278
286
|
* @param grantee The connectable principal to grant connect permissions to
|
|
279
287
|
*/
|
|
280
288
|
grantConnect(grantee: IConnectable): void;
|
|
289
|
+
/**
|
|
290
|
+
* Grant an IAM principal permission to connect as `dbUsername` via RDS IAM
|
|
291
|
+
* database authentication. Instance databases only — Aurora uses a different
|
|
292
|
+
* mechanism, so this throws for non-Instance databases. Requires the instance
|
|
293
|
+
* to be created with `iamAuthentication: true`. The L2 `grantConnect` scopes
|
|
294
|
+
* `rds-db:connect` to the exact `dbuser:<resourceId>/<dbUsername>` ARN — never
|
|
295
|
+
* a bare wildcard. See ADR
|
|
296
|
+
* decisions/2026-06-17-rls-role-auth-and-launch-gating.md.
|
|
297
|
+
*/
|
|
298
|
+
grantIamConnect(grantee: IGrantable, dbUsername: string): Grant;
|
|
281
299
|
}
|
|
282
300
|
export { ClickHouseDatabase, type ClickHouseDatabaseProps };
|
|
283
301
|
export { ClickHouseDefaultProfiles, ClickHouseSchemaAdminSchema, ManagedPasswordNameSchema, ProfileSpecSchema, type ClickHouseSchemaAdmin, type ManagedPasswordName, type ProfileSpec } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
@@ -118,7 +118,7 @@ function validateRelationalDatabaseProps(props) {
|
|
|
118
118
|
"Specify the AWS region where the primary cluster will be created.");
|
|
119
119
|
}
|
|
120
120
|
// Instance-only options on Aurora/GlobalAurora
|
|
121
|
-
warnIfPropertiesIgnored(props, ["readReplica", "multiAz", "allocatedStorage"], "Instance", "Instance database");
|
|
121
|
+
warnIfPropertiesIgnored(props, ["readReplica", "multiAz", "allocatedStorage", "iamAuthentication"], "Instance", "Instance database");
|
|
122
122
|
// Aurora-only options on Instance
|
|
123
123
|
warnIfPropertiesIgnored(props, ["readers", "writer", "backupRetention"], ["Aurora", "GlobalAurora"], "Aurora database");
|
|
124
124
|
// GlobalAurora-only options on non-global databases
|
|
@@ -328,6 +328,8 @@ export class RelationalDatabase extends Construct {
|
|
|
328
328
|
return resolveAlertsTopicShared(this, "AlertsTopic", alertsTopic);
|
|
329
329
|
}
|
|
330
330
|
addDatabase(props) {
|
|
331
|
+
// Re-asserted here (the constructor already validates) to narrow
|
|
332
|
+
// `props.vpc` to `IVpc` for the type-specific add* dispatch below.
|
|
331
333
|
validateRelationalDatabaseProps(props);
|
|
332
334
|
switch (props.type) {
|
|
333
335
|
case "Aurora":
|
|
@@ -440,7 +442,8 @@ export class RelationalDatabase extends Construct {
|
|
|
440
442
|
snapshotUsername: props.snapshotUsername,
|
|
441
443
|
alertsTopic: this.resolveAlertsTopic(props.alertsTopic),
|
|
442
444
|
alarms: props.alarms,
|
|
443
|
-
applicationId: props.applicationId
|
|
445
|
+
applicationId: props.applicationId,
|
|
446
|
+
iamAuthentication: props.iamAuthentication
|
|
444
447
|
});
|
|
445
448
|
this.connections = this.database.connections;
|
|
446
449
|
this.addDatabaseOutputs(props);
|
|
@@ -595,6 +598,22 @@ export class RelationalDatabase extends Construct {
|
|
|
595
598
|
grantConnect(grantee) {
|
|
596
599
|
this.connections.allowDefaultPortFrom(grantee);
|
|
597
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* Grant an IAM principal permission to connect as `dbUsername` via RDS IAM
|
|
603
|
+
* database authentication. Instance databases only — Aurora uses a different
|
|
604
|
+
* mechanism, so this throws for non-Instance databases. Requires the instance
|
|
605
|
+
* to be created with `iamAuthentication: true`. The L2 `grantConnect` scopes
|
|
606
|
+
* `rds-db:connect` to the exact `dbuser:<resourceId>/<dbUsername>` ARN — never
|
|
607
|
+
* a bare wildcard. See ADR
|
|
608
|
+
* decisions/2026-06-17-rls-role-auth-and-launch-gating.md.
|
|
609
|
+
*/
|
|
610
|
+
grantIamConnect(grantee, dbUsername) {
|
|
611
|
+
if (!(this.database instanceof RdsInstance)) {
|
|
612
|
+
throw new Error(`grantIamConnect is only supported for Instance databases; ` +
|
|
613
|
+
`'${this._databaseName}' is a ${this.databaseType} database.`);
|
|
614
|
+
}
|
|
615
|
+
return this.database.grantIamConnect(grantee, dbUsername);
|
|
616
|
+
}
|
|
598
617
|
}
|
|
599
618
|
export { ClickHouseDatabase };
|
|
600
619
|
export { ClickHouseDefaultProfiles, ClickHouseSchemaAdminSchema, ManagedPasswordNameSchema, ProfileSpecSchema } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
@@ -43,8 +43,13 @@ export class Ec2GracefulTerminationHandler extends Construct {
|
|
|
43
43
|
const dataVolumeOwnerLogicalId = resolveOptionalString(props.dataVolumeOwnerLogicalId);
|
|
44
44
|
this.queue = new SQSQueue(this, `${id}Queue`, {
|
|
45
45
|
visibilityTimeout: QUEUE_VISIBILITY_TIMEOUT_SECONDS,
|
|
46
|
-
deadLetterQueue: { enabled: true, maxReceiveCount: 5 }
|
|
46
|
+
deadLetterQueue: { enabled: true, maxReceiveCount: 5 },
|
|
47
|
+
// Transient instance-drain signals — no durable state. Pinned DESTROY
|
|
48
|
+
// (now also the SQSQueue wrapper default) so a replacing deploy of the
|
|
49
|
+
// parent Ec2Instance reclaims this queue + DLQ instead of orphaning them.
|
|
50
|
+
removalPolicy: "DESTROY"
|
|
47
51
|
});
|
|
52
|
+
const stack = Stack.of(this);
|
|
48
53
|
const ecsPolicies = ecsClusterArn !== undefined
|
|
49
54
|
? [
|
|
50
55
|
new PolicyStatement({
|
|
@@ -66,6 +71,9 @@ export class Ec2GracefulTerminationHandler extends Construct {
|
|
|
66
71
|
})
|
|
67
72
|
]
|
|
68
73
|
: [];
|
|
74
|
+
// EIP/ENI mutations cannot be ARN-scoped (the resources are not known at
|
|
75
|
+
// synth), but the Lambda only ever drains instances in its own region — a
|
|
76
|
+
// region clamp removes the cross-region blast radius a bare "*" would allow.
|
|
69
77
|
const ec2Policies = new PolicyStatement({
|
|
70
78
|
effect: Effect.ALLOW,
|
|
71
79
|
actions: [
|
|
@@ -74,7 +82,10 @@ export class Ec2GracefulTerminationHandler extends Construct {
|
|
|
74
82
|
"ec2:DescribeNetworkInterfaces",
|
|
75
83
|
"ec2:DetachNetworkInterface"
|
|
76
84
|
],
|
|
77
|
-
resources: ["*"]
|
|
85
|
+
resources: ["*"],
|
|
86
|
+
conditions: {
|
|
87
|
+
StringEquals: { "aws:RequestedRegion": stack.region }
|
|
88
|
+
}
|
|
78
89
|
});
|
|
79
90
|
const elbReadPolicy = new PolicyStatement({
|
|
80
91
|
effect: Effect.ALLOW,
|
|
@@ -94,7 +105,6 @@ export class Ec2GracefulTerminationHandler extends Construct {
|
|
|
94
105
|
}
|
|
95
106
|
}
|
|
96
107
|
});
|
|
97
|
-
const stack = Stack.of(this);
|
|
98
108
|
// Account/region-scoped wildcard rather than the specific ASG ARN —
|
|
99
109
|
// see PersistentDataVolume for the full deadlock writeup. Same gotcha:
|
|
100
110
|
// a specific ARN creates a CFN Ref to the ASG, the Lambda's IAM Policy
|
|
@@ -4,7 +4,7 @@ import { Construct } from "constructs";
|
|
|
4
4
|
import { CfnOutput, Aspects } from "aws-cdk-lib";
|
|
5
5
|
import { processConnections } from "../../../utils/connections.js";
|
|
6
6
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
7
|
-
import { createEcsServiceAlarms } from "../monitoring/index.js";
|
|
7
|
+
import { createEcsServiceAlarms, createLogPatternAlarms } from "../monitoring/index.js";
|
|
8
8
|
// Extracted modules
|
|
9
9
|
import { CapacityProviderDependencyAspect } from "./ecsCapacityProviderAspect.js";
|
|
10
10
|
import { validateEcsClusterProps } from "./ecsValidation.js";
|
|
@@ -210,7 +210,7 @@ export default class EcsCluster extends Construct {
|
|
|
210
210
|
const executionRole = createExecutionRole(this.ctx, serviceName);
|
|
211
211
|
const taskRole = createTaskRole(this.ctx, serviceName, serviceProps);
|
|
212
212
|
const taskDefinition = createTaskDefinition(this.ctx, serviceName, serviceProps, executionRole, taskRole);
|
|
213
|
-
const { containers, primaryContainer } = addContainersToTask(this.ctx, serviceName, serviceProps, taskDefinition);
|
|
213
|
+
const { containers, primaryContainer, logGroup } = addContainersToTask(this.ctx, serviceName, serviceProps, taskDefinition);
|
|
214
214
|
const service = createService(this.ctx, serviceName, serviceProps, taskDefinition, this.asgState);
|
|
215
215
|
let targetGroup;
|
|
216
216
|
if (!this.loadBalancerDisabled &&
|
|
@@ -230,7 +230,8 @@ export default class EcsCluster extends Construct {
|
|
|
230
230
|
containers,
|
|
231
231
|
primaryContainer,
|
|
232
232
|
targetGroup,
|
|
233
|
-
scalingPolicy
|
|
233
|
+
scalingPolicy,
|
|
234
|
+
logGroup
|
|
234
235
|
});
|
|
235
236
|
if (serviceProps.connections && serviceProps.connections.length > 0) {
|
|
236
237
|
try {
|
|
@@ -253,6 +254,16 @@ export default class EcsCluster extends Construct {
|
|
|
253
254
|
alarmTopic: this.props.alertsTopic,
|
|
254
255
|
applicationId: this.props.applicationId
|
|
255
256
|
});
|
|
257
|
+
if (serviceProps.logAlarms && serviceProps.logAlarms.length > 0) {
|
|
258
|
+
createLogPatternAlarms({
|
|
259
|
+
scope: this,
|
|
260
|
+
logGroup,
|
|
261
|
+
alarmTopic: this.props.alertsTopic,
|
|
262
|
+
metricNamespace: serviceProps.logMetricNamespace,
|
|
263
|
+
specs: serviceProps.logAlarms,
|
|
264
|
+
applicationId: this.props.applicationId
|
|
265
|
+
});
|
|
266
|
+
}
|
|
256
267
|
}
|
|
257
268
|
}
|
|
258
269
|
setupConnections(props) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { AmiHardwareType } from "aws-cdk-lib/aws-ecs";
|
|
2
|
+
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
2
3
|
export declare const DEFAULT_EC2_INSTANCE_TYPE = "t4g.micro";
|
|
3
4
|
export declare const DEFAULT_WARM_POOL_MIN_SIZE = 1;
|
|
4
5
|
export declare const DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN = false;
|
|
5
6
|
export declare const DEFAULT_LOG_RETENTION_DAYS = 14;
|
|
7
|
+
export declare const DEFAULT_LOG_RETENTION = RetentionDays.TWO_WEEKS;
|
|
6
8
|
export declare const DEFAULT_FARGATE_CPU = 256;
|
|
7
9
|
export declare const DEFAULT_FARGATE_MEMORY_MIB = 512;
|
|
8
10
|
export declare const DEFAULT_EC2_CONTAINER_MEMORY_MIB = 1024;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AmiHardwareType } from "aws-cdk-lib/aws-ecs";
|
|
2
|
+
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
2
3
|
// Canonical source: @fjall/generator schemas/constants.ts — keep in sync
|
|
3
4
|
export const DEFAULT_EC2_INSTANCE_TYPE = "t4g.micro";
|
|
4
5
|
export const DEFAULT_WARM_POOL_MIN_SIZE = 1;
|
|
@@ -8,6 +9,9 @@ export const DEFAULT_WARM_POOL_MIN_SIZE = 1;
|
|
|
8
9
|
export const DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN = false;
|
|
9
10
|
// 14 days balances cost against retaining enough history for post-mortem debugging
|
|
10
11
|
export const DEFAULT_LOG_RETENTION_DAYS = 14;
|
|
12
|
+
// The RetentionDays enum the explicit service LogGroup needs. Coupled to
|
|
13
|
+
// DEFAULT_LOG_RETENTION_DAYS (TWO_WEEKS === 14) — the two must move together.
|
|
14
|
+
export const DEFAULT_LOG_RETENTION = RetentionDays.TWO_WEEKS;
|
|
11
15
|
// Smallest valid (cpu, memory) pair on the Fargate matrix — must move together.
|
|
12
16
|
export const DEFAULT_FARGATE_CPU = 256;
|
|
13
17
|
export const DEFAULT_FARGATE_MEMORY_MIB = 512;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FargateTaskDefinition, Ec2TaskDefinition, NetworkMode, type ContainerDefinition } from "aws-cdk-lib/aws-ecs";
|
|
2
2
|
import type { Construct } from "constructs";
|
|
3
|
+
import type { ILogGroup } from "aws-cdk-lib/aws-logs";
|
|
3
4
|
import { type Role } from "aws-cdk-lib/aws-iam";
|
|
4
5
|
import type { EcsConstructContext } from "./ecsContext.js";
|
|
5
6
|
import type { EcsClusterProps, EcsServiceProps, EcsCapacityProvider } from "./ecsTypes.js";
|
|
@@ -52,4 +53,5 @@ export declare function createMigrationTaskDefinition(scope: Construct, id: stri
|
|
|
52
53
|
export declare function addContainersToTask(ctx: EcsConstructContext, serviceName: string, serviceProps: EcsServiceProps, taskDefinition: FargateTaskDefinition | Ec2TaskDefinition): {
|
|
53
54
|
containers: ContainerDefinition[];
|
|
54
55
|
primaryContainer?: ContainerDefinition;
|
|
56
|
+
logGroup: ILogGroup;
|
|
55
57
|
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { AwsLogDriver, ContainerDependencyCondition, FargateTaskDefinition, Ec2TaskDefinition, NetworkMode, CpuArchitecture, OperatingSystemFamily } from "aws-cdk-lib/aws-ecs";
|
|
2
|
-
import { Duration } from "aws-cdk-lib";
|
|
2
|
+
import { Duration, RemovalPolicy } from "aws-cdk-lib";
|
|
3
3
|
import { Secret as EcsSecret } from "aws-cdk-lib/aws-ecs";
|
|
4
4
|
import { StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
5
5
|
import { resolveOrgId } from "../../../utils/cdkContext.js";
|
|
6
6
|
import { validateSsmPathComponent } from "./ecsValidation.js";
|
|
7
|
-
import {
|
|
7
|
+
import { DEFAULT_LOG_RETENTION, DEFAULT_FARGATE_CPU, DEFAULT_FARGATE_MEMORY_MIB, DEFAULT_EC2_CONTAINER_MEMORY_MIB } from "./ecsConstants.js";
|
|
8
|
+
import { LogGroup } from "../logging/logGroup.js";
|
|
8
9
|
import { getContainerImage } from "./ecsImages.js";
|
|
9
10
|
import { resolveRemoteConnections } from "./ecsRemoteConnections.js";
|
|
10
11
|
import { resolveImportedSecret } from "../secrets/index.js";
|
|
@@ -124,6 +125,14 @@ export function addContainersToTask(ctx, serviceName, serviceProps, taskDefiniti
|
|
|
124
125
|
const orgId = resolveOrgId(ctx.scope.node);
|
|
125
126
|
const remoteEnvByService = resolveRemoteConnections([serviceProps], ctx.scope, orgId);
|
|
126
127
|
const remoteEnv = remoteEnvByService[serviceName] ?? {};
|
|
128
|
+
// Explicit so the cluster can attach metric filters (createLogPatternAlarms).
|
|
129
|
+
// No logGroupName: CDK auto-names from the logical ID so it cannot collide
|
|
130
|
+
// with the orphaned-retained old group during the implicit→explicit replace.
|
|
131
|
+
// RETAIN (never DESTROY): preserves fail-closed evidence and supports rollback.
|
|
132
|
+
const logGroup = new LogGroup(ctx.scope, `${ctx.props.clusterName}${serviceName}LogGroup`, {
|
|
133
|
+
retention: DEFAULT_LOG_RETENTION,
|
|
134
|
+
removalPolicy: RemovalPolicy.RETAIN
|
|
135
|
+
});
|
|
127
136
|
for (const containerConfig of serviceProps.containers) {
|
|
128
137
|
const image = getContainerImage(ctx, serviceName, containerConfig, serviceProps);
|
|
129
138
|
const isFirstWithPort = !primaryContainer && containerConfig.port !== undefined;
|
|
@@ -156,7 +165,7 @@ export function addContainersToTask(ctx, serviceName, serviceProps, taskDefiniti
|
|
|
156
165
|
containerName: containerConfig.name,
|
|
157
166
|
logging: new AwsLogDriver({
|
|
158
167
|
streamPrefix: `/ecs/${ctx.props.clusterName}/${serviceName}`,
|
|
159
|
-
|
|
168
|
+
logGroup
|
|
160
169
|
}),
|
|
161
170
|
// remoteEnv (cross-app `${PREFIX}_HOST/_PORT`) intentionally overrides user
|
|
162
171
|
// values — a stale manual setting must not mask the resolved peer.
|
|
@@ -250,5 +259,5 @@ export function addContainersToTask(ctx, serviceName, serviceProps, taskDefiniti
|
|
|
250
259
|
});
|
|
251
260
|
}
|
|
252
261
|
}
|
|
253
|
-
return { containers, primaryContainer };
|
|
262
|
+
return { containers, primaryContainer, logGroup };
|
|
254
263
|
}
|
|
@@ -19,7 +19,8 @@ import { type RemoteConnectionSpec } from "./ecsRemoteConnections.js";
|
|
|
19
19
|
import { type SecretImport } from "../secrets/index.js";
|
|
20
20
|
import type { ManagedDomainExports } from "../../../utils/domainTypes.js";
|
|
21
21
|
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
22
|
-
import type {
|
|
22
|
+
import type { ILogGroup } from "aws-cdk-lib/aws-logs";
|
|
23
|
+
import type { EcsServiceAlarmThresholds, LogPatternAlarmSpec } from "../monitoring/index.js";
|
|
23
24
|
import { type Ec2InstancePersistentDataVolumeConfig } from "./ec2.js";
|
|
24
25
|
export declare enum Protocol {
|
|
25
26
|
HTTP = 0,
|
|
@@ -437,6 +438,19 @@ export interface EcsServiceProps {
|
|
|
437
438
|
* - object: override specific thresholds
|
|
438
439
|
*/
|
|
439
440
|
alarms?: EcsServiceAlarmThresholds | false;
|
|
441
|
+
/**
|
|
442
|
+
* Log-pattern alarms for this service's log group. Each spec creates a
|
|
443
|
+
* CloudWatch metric filter + alarm (see `createLogPatternAlarms`). Materialised
|
|
444
|
+
* only when the cluster carries `alertsTopic` and `alarms !== false` — the same
|
|
445
|
+
* gate as the per-service metric alarms.
|
|
446
|
+
*/
|
|
447
|
+
logAlarms?: LogPatternAlarmSpec[];
|
|
448
|
+
/**
|
|
449
|
+
* Default CloudWatch metric namespace for this service's `logAlarms`. Each
|
|
450
|
+
* spec may override it per-entry, so one service can emit alarms in different
|
|
451
|
+
* namespaces (e.g. `Fjall/WebApp` for RLS, `Fjall/ClickHouse` for stuck-merge).
|
|
452
|
+
*/
|
|
453
|
+
logMetricNamespace?: string;
|
|
440
454
|
/**
|
|
441
455
|
* Deployment circuit breaker policy.
|
|
442
456
|
* - undefined (default): `{ enable: true, rollback: true }`
|
|
@@ -543,4 +557,6 @@ export interface ServiceData {
|
|
|
543
557
|
primaryContainer?: ContainerDefinition;
|
|
544
558
|
targetGroup?: IApplicationTargetGroup;
|
|
545
559
|
scalingPolicy?: TargetTrackingScalingPolicy | StepScalingPolicy;
|
|
560
|
+
/** Explicit log group shared by all of the service's containers. */
|
|
561
|
+
logGroup: ILogGroup;
|
|
546
562
|
}
|
|
@@ -12,6 +12,13 @@ import type { EcsClusterProps } from "./ecsTypes.js";
|
|
|
12
12
|
* layer consumers never see a `migrations` field, so duplicating the
|
|
13
13
|
* validation here would be unreachable.
|
|
14
14
|
*
|
|
15
|
+
* Same applies to `service.awaitMigrationsFrom`: it is a patterns-layer
|
|
16
|
+
* cross-service ordering knob, resolved into a `node.addDependency(...)` edge
|
|
17
|
+
* (`wireServiceMigrationDependencies` in `computeEcs.ts`) BEFORE reaching the
|
|
18
|
+
* resources layer. It is not a field on `EcsServiceProps`, so a direct
|
|
19
|
+
* `new EcsCluster(...)` consumer cannot pass it — there is no resources-layer
|
|
20
|
+
* code path to validate.
|
|
21
|
+
*
|
|
15
22
|
* @param props - The cluster props to validate
|
|
16
23
|
* @throws Error if validation fails
|
|
17
24
|
*/
|