@fjall/components-infrastructure 3.3.0 → 3.4.1
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/resources/aws/compute/ecsServiceFactory.d.ts +3 -3
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +60 -18
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +26 -2
- package/dist/lib/resources/aws/networking/domainCertificate.js +5 -2
- package/dist/lib/utils/acmTagValue.d.ts +2 -0
- package/dist/lib/utils/acmTagValue.js +13 -0
- package/dist/lib/utils/costAllocationTags.d.ts +2 -7
- package/dist/lib/utils/costAllocationTags.js +5 -7
- package/package.json +4 -4
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { FargateService, Ec2Service, AsgCapacityProvider, type DeploymentCircuitBreaker } from "aws-cdk-lib/aws-ecs";
|
|
2
2
|
import type { FargateTaskDefinition, Ec2TaskDefinition } from "aws-cdk-lib/aws-ecs";
|
|
3
3
|
import { type ISecurityGroup } from "aws-cdk-lib/aws-ec2";
|
|
4
|
-
import { TargetTrackingScalingPolicy
|
|
4
|
+
import { TargetTrackingScalingPolicy } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
5
5
|
import { type AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
|
|
6
|
-
import { type EcsServiceProps, type Ec2CapacityConfig } from "./ecsTypes.js";
|
|
6
|
+
import { type EcsServiceProps, type Ec2CapacityConfig, type QueueScalingResources } from "./ecsTypes.js";
|
|
7
7
|
import type { EcsConstructContext } from "./ecsContext.js";
|
|
8
8
|
/**
|
|
9
9
|
* Resolves the user's `circuitBreaker` config to the CDK
|
|
@@ -41,4 +41,4 @@ export declare function createService(ctx: EcsConstructContext, serviceName: str
|
|
|
41
41
|
* policy on utilisation; `QUEUE` emits a step-scaling policy on SQS backlog
|
|
42
42
|
* that can wake the service from `desiredCount: 0`.
|
|
43
43
|
*/
|
|
44
|
-
export declare function addServiceScaling(ctx: EcsConstructContext, serviceName: string, serviceProps: EcsServiceProps, service: FargateService | Ec2Service): TargetTrackingScalingPolicy |
|
|
44
|
+
export declare function addServiceScaling(ctx: EcsConstructContext, serviceName: string, serviceProps: EcsServiceProps, service: FargateService | Ec2Service): TargetTrackingScalingPolicy | QueueScalingResources;
|
|
@@ -3,8 +3,9 @@ import { Peer, Port, SubnetType, UserData } from "aws-cdk-lib/aws-ec2";
|
|
|
3
3
|
import { ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import { Role } from "../iam/role.js";
|
|
5
5
|
import { CfnOutput, Duration, Token } from "aws-cdk-lib";
|
|
6
|
-
import { AdjustmentType, PredefinedMetric, ScalableTarget, ServiceNamespace, TargetTrackingScalingPolicy } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
7
|
-
import { MathExpression } from "aws-cdk-lib/aws-cloudwatch";
|
|
6
|
+
import { AdjustmentType, PredefinedMetric, ScalableTarget, ServiceNamespace, StepScalingAction, TargetTrackingScalingPolicy } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
7
|
+
import { Alarm, ComparisonOperator, MathExpression } from "aws-cdk-lib/aws-cloudwatch";
|
|
8
|
+
import { ApplicationScalingAction } from "aws-cdk-lib/aws-cloudwatch-actions";
|
|
8
9
|
import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
|
|
9
10
|
import { SecurityGroup } from "../networking/securityGroup.js";
|
|
10
11
|
import { Ec2Instance } from "./ec2.js";
|
|
@@ -283,13 +284,14 @@ export function createService(ctx, serviceName, serviceProps, taskDefinition, as
|
|
|
283
284
|
return service;
|
|
284
285
|
}
|
|
285
286
|
/**
|
|
286
|
-
*
|
|
287
|
-
* `AdjustmentType.EXACT_CAPACITY` (`
|
|
288
|
-
* Backlog
|
|
289
|
-
*
|
|
287
|
+
* Wake-ladder intervals on the queue-backlog metric, with
|
|
288
|
+
* `AdjustmentType.EXACT_CAPACITY` (`capacity` is the absolute DesiredCount).
|
|
289
|
+
* Backlog ≥1 → 1 task; ≥100 → 2; ≥500 → 3. Capacities are clamped to the
|
|
290
|
+
* service's `maxCapacity` at build time. Bounds are absolute backlog values;
|
|
291
|
+
* `buildQueueScalingPolicy` re-expresses them relative to the wake alarm's
|
|
292
|
+
* threshold (the first interval's `lower`).
|
|
290
293
|
*/
|
|
291
|
-
const
|
|
292
|
-
{ lower: 0, upper: 1, capacity: 0 },
|
|
294
|
+
const QUEUE_WAKE_SCALING_STEPS = [
|
|
293
295
|
{ lower: 1, upper: 100, capacity: 1 },
|
|
294
296
|
{ lower: 100, upper: 500, capacity: 2 },
|
|
295
297
|
{ lower: 500, capacity: 3 }
|
|
@@ -327,11 +329,21 @@ export function addServiceScaling(ctx, serviceName, serviceProps, service) {
|
|
|
327
329
|
});
|
|
328
330
|
}
|
|
329
331
|
/**
|
|
330
|
-
* Builds the step-scaling
|
|
331
|
-
* `MathExpression` SUM of `ApproximateNumberOfMessagesVisible`
|
|
332
|
+
* Builds the wake/sleep step-scaling pair for `ScalingType.QUEUE`. The alarm
|
|
333
|
+
* metric is a `MathExpression` SUM of `ApproximateNumberOfMessagesVisible`
|
|
332
334
|
* (+`…NotVisible` when `includeInFlight`) across the consumed queues, sampled at
|
|
333
335
|
* `Maximum` over 1-minute periods — published independent of running-task count,
|
|
334
|
-
* so the
|
|
336
|
+
* so the alarms fire (and DesiredCount rises from 0) on backlog alone.
|
|
337
|
+
*
|
|
338
|
+
* The actions and alarms are wired explicitly rather than via
|
|
339
|
+
* `ScalableTarget.scaleOnMetric`: CDK's `StepScalingPolicy` middle-splits an
|
|
340
|
+
* all-`EXACT_CAPACITY` interval ladder to place its two alarm thresholds
|
|
341
|
+
* (`findAlarmThresholds` in aws-autoscaling-common), which for a
|
|
342
|
+
* 0/1/100/500 ladder puts BOTH thresholds at 100. Step policies are only
|
|
343
|
+
* invoked on alarm state transitions, so backlog 1–99 produced no transition
|
|
344
|
+
* and could never wake a zero-task service. The explicit pair pins the wake
|
|
345
|
+
* threshold at the ladder's first bound (backlog ≥ 1) and the sleep threshold
|
|
346
|
+
* at 0, making every 0↔1 backlog crossing a state transition.
|
|
335
347
|
*/
|
|
336
348
|
function buildQueueScalingPolicy(serviceName, config, scalableTarget, maxCapacity) {
|
|
337
349
|
if (config === undefined || config.queues.length === 0) {
|
|
@@ -363,16 +375,46 @@ function buildQueueScalingPolicy(serviceName, config, scalableTarget, maxCapacit
|
|
|
363
375
|
period: Duration.minutes(1),
|
|
364
376
|
label: `${serviceName}QueueBacklog`
|
|
365
377
|
});
|
|
366
|
-
|
|
378
|
+
const cooldown = config.cooldown ?? DEFAULT_QUEUE_SCALE_COOLDOWN;
|
|
379
|
+
const wakeThreshold = QUEUE_WAKE_SCALING_STEPS[0]?.lower ?? 1;
|
|
380
|
+
const wakeAction = new StepScalingAction(scalableTarget, `${serviceName}QueueWakeAction`, {
|
|
381
|
+
scalingTarget: scalableTarget,
|
|
382
|
+
adjustmentType: AdjustmentType.EXACT_CAPACITY,
|
|
383
|
+
cooldown
|
|
384
|
+
});
|
|
385
|
+
QUEUE_WAKE_SCALING_STEPS.forEach((step, index) => {
|
|
386
|
+
wakeAction.addAdjustment({
|
|
387
|
+
adjustment: Math.min(step.capacity, maxCapacity),
|
|
388
|
+
lowerBound: step.lower - wakeThreshold,
|
|
389
|
+
...(index < QUEUE_WAKE_SCALING_STEPS.length - 1 &&
|
|
390
|
+
step.upper !== undefined && { upperBound: step.upper - wakeThreshold })
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
const wakeAlarm = new Alarm(scalableTarget, `${serviceName}QueueWakeAlarm`, {
|
|
367
394
|
metric: backlogMetric,
|
|
395
|
+
alarmDescription: `Scale out '${serviceName}' on queue backlog`,
|
|
396
|
+
comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
|
|
397
|
+
threshold: wakeThreshold,
|
|
398
|
+
evaluationPeriods: 1,
|
|
399
|
+
datapointsToAlarm: 1
|
|
400
|
+
});
|
|
401
|
+
wakeAlarm.addAlarmAction(new ApplicationScalingAction(wakeAction));
|
|
402
|
+
const sleepAction = new StepScalingAction(scalableTarget, `${serviceName}QueueSleepAction`, {
|
|
403
|
+
scalingTarget: scalableTarget,
|
|
368
404
|
adjustmentType: AdjustmentType.EXACT_CAPACITY,
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
405
|
+
cooldown
|
|
406
|
+
});
|
|
407
|
+
// upperBound is relative to the alarm threshold (0), so this single
|
|
408
|
+
// open-ended interval covers exactly backlog ≤ 0.
|
|
409
|
+
sleepAction.addAdjustment({ adjustment: 0, upperBound: 0 });
|
|
410
|
+
const sleepAlarm = new Alarm(scalableTarget, `${serviceName}QueueSleepAlarm`, {
|
|
411
|
+
metric: backlogMetric,
|
|
412
|
+
alarmDescription: `Scale '${serviceName}' to zero on empty queue`,
|
|
413
|
+
comparisonOperator: ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD,
|
|
414
|
+
threshold: 0,
|
|
375
415
|
evaluationPeriods: 1,
|
|
376
416
|
datapointsToAlarm: 1
|
|
377
417
|
});
|
|
418
|
+
sleepAlarm.addAlarmAction(new ApplicationScalingAction(sleepAction));
|
|
419
|
+
return { wakeAction, wakeAlarm, sleepAction, sleepAlarm };
|
|
378
420
|
}
|
|
@@ -4,7 +4,8 @@ import { type Monitoring } from "aws-cdk-lib/aws-autoscaling";
|
|
|
4
4
|
import { type IService } from "aws-cdk-lib/aws-servicediscovery";
|
|
5
5
|
import { type IManagedPolicy, type PolicyDocument } from "aws-cdk-lib/aws-iam";
|
|
6
6
|
import type { DockerBuild } from "@fjall/util/manifest/schemas";
|
|
7
|
-
import { type TargetTrackingScalingPolicy, type
|
|
7
|
+
import { type TargetTrackingScalingPolicy, type StepScalingAction } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
8
|
+
import { type Alarm } from "aws-cdk-lib/aws-cloudwatch";
|
|
8
9
|
import { type IQueue } from "aws-cdk-lib/aws-sqs";
|
|
9
10
|
import { type Duration } from "aws-cdk-lib";
|
|
10
11
|
import { type GeoLocation } from "aws-cdk-lib/aws-route53";
|
|
@@ -63,6 +64,29 @@ export interface QueueScalingConfig {
|
|
|
63
64
|
/** Cooldown after a scale action. Default: 5 minutes. */
|
|
64
65
|
cooldown?: Duration;
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The explicitly-wired wake/sleep alarm pair emitted for `ScalingType.QUEUE`.
|
|
69
|
+
*
|
|
70
|
+
* Built by hand (NOT via `ScalableTarget.scaleOnMetric`) because CDK's
|
|
71
|
+
* `StepScalingPolicy` places its two alarm thresholds by middle-splitting the
|
|
72
|
+
* interval ladder when every `EXACT_CAPACITY` step carries a change — for the
|
|
73
|
+
* default 4-step ladder that puts BOTH thresholds at backlog 100, and since
|
|
74
|
+
* step policies are only invoked on alarm state transitions, a backlog of
|
|
75
|
+
* 1–99 can never wake a `desiredCount: 0` service (observed live: the prod
|
|
76
|
+
* deploy-worker sat at 0 tasks with 1 queued message indefinitely). The
|
|
77
|
+
* explicit pair pins the wake boundary at backlog ≥ 1 and the sleep boundary
|
|
78
|
+
* at backlog 0.
|
|
79
|
+
*/
|
|
80
|
+
export interface QueueScalingResources {
|
|
81
|
+
/** Scale-out action: backlog ≥ 1 → capacity ladder (1/2/3, clamped). */
|
|
82
|
+
wakeAction: StepScalingAction;
|
|
83
|
+
/** Alarm at backlog ≥ 1 driving `wakeAction`. */
|
|
84
|
+
wakeAlarm: Alarm;
|
|
85
|
+
/** Scale-in action: backlog 0 → capacity 0. */
|
|
86
|
+
sleepAction: StepScalingAction;
|
|
87
|
+
/** Alarm at backlog ≤ 0 driving `sleepAction`. */
|
|
88
|
+
sleepAlarm: Alarm;
|
|
89
|
+
}
|
|
66
90
|
import type { EcsCapacityProvider } from "@fjall/generator";
|
|
67
91
|
export type { EcsCapacityProvider };
|
|
68
92
|
/**
|
|
@@ -607,7 +631,7 @@ export interface ServiceData {
|
|
|
607
631
|
containers: ContainerDefinition[];
|
|
608
632
|
primaryContainer?: ContainerDefinition;
|
|
609
633
|
targetGroup?: IApplicationTargetGroup;
|
|
610
|
-
scalingPolicy?: TargetTrackingScalingPolicy |
|
|
634
|
+
scalingPolicy?: TargetTrackingScalingPolicy | QueueScalingResources;
|
|
611
635
|
/** Explicit log group shared by all of the service's containers. */
|
|
612
636
|
logGroup: ILogGroup;
|
|
613
637
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CfnOutput, Tags } from "aws-cdk-lib";
|
|
2
2
|
import { Certificate, CertificateValidation } from "aws-cdk-lib/aws-certificatemanager";
|
|
3
3
|
import { getDomainExportNames } from "@fjall/util";
|
|
4
|
+
import { sanitiseAcmTagValue } from "../../../utils/acmTagValue.js";
|
|
4
5
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
5
6
|
import { applyCostAllocationTags } from "../../../utils/costAllocationTags.js";
|
|
6
7
|
/**
|
|
@@ -33,10 +34,12 @@ export class DomainCertificate extends Certificate {
|
|
|
33
34
|
this.description =
|
|
34
35
|
props.description ??
|
|
35
36
|
`Fjall-managed ACM certificate for ${props.domainName}`;
|
|
36
|
-
|
|
37
|
+
// ACM's tag-value charset excludes `*`, so a wildcard domainName in either
|
|
38
|
+
// tag fails CreateCertificate — sanitise both (see utils/acmTagValue.ts).
|
|
39
|
+
Tags.of(this).add("fjall:description", sanitiseAcmTagValue(this.description));
|
|
37
40
|
applyCostAllocationTags(this, {
|
|
38
41
|
service: "certificate",
|
|
39
|
-
domain: props.costAllocationDomain ?? props.domainName,
|
|
42
|
+
domain: sanitiseAcmTagValue(props.costAllocationDomain ?? props.domainName),
|
|
40
43
|
environment: props.costAllocationEnvironment,
|
|
41
44
|
inheritEnvironment: props.inheritCostAllocationEnvironment
|
|
42
45
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACM rejects tag values containing characters outside
|
|
3
|
+
* `[\p{L}\p{Z}\p{N}_.:/=+\-@]*` (letters, spaces, numbers, `_ . : / = + - @`)
|
|
4
|
+
* with a CreateCertificate ValidationException — notably `*`, so any tag
|
|
5
|
+
* embedding a wildcard domain (`*.dev.example.com`) fails the whole stack at
|
|
6
|
+
* deploy time. Most other taggable services (S3, EC2, Route53) accept `*`,
|
|
7
|
+
* which is why the constraint only surfaces at the certificate boundary.
|
|
8
|
+
*/
|
|
9
|
+
const ACM_TAG_VALUE_DISALLOWED = /[^\p{L}\p{Z}\p{N}_.:/=+\-@]/gu;
|
|
10
|
+
/** Replace every ACM-disallowed character with `_` (an allowed character). */
|
|
11
|
+
export function sanitiseAcmTagValue(value) {
|
|
12
|
+
return value.replace(ACM_TAG_VALUE_DISALLOWED, "_");
|
|
13
|
+
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
+
import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "@fjall/util/aws";
|
|
1
2
|
import type { IConstruct } from "constructs";
|
|
2
|
-
export
|
|
3
|
-
readonly ENVIRONMENT: "fjall:costAllocation:environment";
|
|
4
|
-
readonly SERVICE: "fjall:costAllocation:service";
|
|
5
|
-
readonly DOMAIN: "fjall:costAllocation:domain";
|
|
6
|
-
readonly OWNER: "fjall:costAllocation:owner";
|
|
7
|
-
};
|
|
8
|
-
export declare const DEFAULT_COST_ALLOCATION_ENVIRONMENT: "management";
|
|
3
|
+
export { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT };
|
|
9
4
|
export interface CostAllocationTagsArgs {
|
|
10
5
|
readonly service: string;
|
|
11
6
|
readonly domain: string;
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
+
import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "@fjall/util/aws";
|
|
1
2
|
import { Tags } from "aws-cdk-lib";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
OWNER: "fjall:costAllocation:owner"
|
|
7
|
-
};
|
|
8
|
-
export const DEFAULT_COST_ALLOCATION_ENVIRONMENT = "management";
|
|
3
|
+
// Canonical home: @fjall/util (`util/src/aws/costAllocationTags.ts`) — shared
|
|
4
|
+
// with deploy-core's `cdk bootstrap --tags` stamping. Re-exported here so
|
|
5
|
+
// construct-package consumers keep their existing import path.
|
|
6
|
+
export { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT };
|
|
9
7
|
export function applyCostAllocationTags(scope, args) {
|
|
10
8
|
if (args.environment !== undefined) {
|
|
11
9
|
Tags.of(scope).add(COST_ALLOCATION_TAGS.ENVIRONMENT, args.environment);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.1",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
70
|
-
"@fjall/generator": "^3.
|
|
71
|
-
"@fjall/util": "^3.
|
|
70
|
+
"@fjall/generator": "^3.4.1",
|
|
71
|
+
"@fjall/util": "^3.4.1",
|
|
72
72
|
"constructs": "^10.6.0"
|
|
73
73
|
},
|
|
74
74
|
"overrides": {
|
|
@@ -82,5 +82,5 @@
|
|
|
82
82
|
"engines": {
|
|
83
83
|
"node": ">=18.0.0"
|
|
84
84
|
},
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "6d8ef9582d5abe6a8199431815dae761887c18d4"
|
|
86
86
|
}
|