@fjall/components-infrastructure 2.9.1 → 2.12.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/disasterRecovery.js +23 -4
- package/dist/lib/config/aws/ecrDefaultImage.js +2 -2
- package/dist/lib/config/aws/s3BlockPublicAccess.d.ts +22 -4
- package/dist/lib/config/aws/s3BlockPublicAccess.js +33 -13
- package/dist/lib/config/aws/scpPreset.js +6 -1
- package/dist/lib/patterns/aws/account.d.ts +15 -0
- package/dist/lib/patterns/aws/account.js +32 -6
- package/dist/lib/patterns/aws/buildkite.js +1 -1
- package/dist/lib/patterns/aws/clickhouseDatabase.js +5 -2
- package/dist/lib/patterns/aws/compute.d.ts +22 -0
- package/dist/lib/patterns/aws/compute.js +42 -0
- package/dist/lib/patterns/aws/computeEcs.d.ts +2 -1
- package/dist/lib/patterns/aws/computeEcs.js +67 -24
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +8 -2
- package/dist/lib/patterns/aws/organisation.d.ts +19 -8
- package/dist/lib/patterns/aws/organisation.js +23 -10
- package/dist/lib/patterns/aws/organisationFactory.js +2 -1
- package/dist/lib/patterns/aws/platform.d.ts +1 -0
- package/dist/lib/patterns/aws/platform.js +3 -0
- package/dist/lib/resources/aws/backup/backupVault.js +5 -3
- package/dist/lib/resources/aws/compute/ecsConstants.d.ts +1 -1
- package/dist/lib/resources/aws/compute/ecsConstants.js +4 -1
- package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.js +2 -2
- package/dist/lib/resources/aws/compute/ecsRoles.js +4 -13
- package/dist/lib/resources/aws/compute/ecsServiceFactory.d.ts +5 -3
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +76 -3
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.d.ts +0 -5
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +2 -20
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +50 -8
- package/dist/lib/resources/aws/compute/ecsTypes.js +11 -0
- package/dist/lib/resources/aws/compute/ecsValidation.js +37 -0
- package/dist/lib/resources/aws/compute/lambda.d.ts +11 -0
- package/dist/lib/resources/aws/compute/lambda.js +23 -3
- package/dist/lib/resources/aws/secrets/parameter.js +3 -3
- package/dist/lib/resources/aws/secrets/secret.d.ts +32 -11
- package/dist/lib/resources/aws/secrets/secret.js +53 -0
- package/dist/lib/resources/aws/utilities/customResource.d.ts +4 -0
- package/dist/lib/resources/aws/utilities/customResource.js +4 -0
- package/dist/lib/utils/env.js +2 -2
- package/dist/lib/utils/getConfig.js +2 -2
- package/dist/lib/utils/orgConfigParser.js +16 -2
- package/package.json +4 -4
|
@@ -7,13 +7,13 @@ import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
|
7
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
|
-
import { ScalingType, type DomainConfig, type EcsCapacityProvider, type Ec2CapacityConfig } from "../../resources/aws/compute/ecs.js";
|
|
10
|
+
import { ScalingType, type DomainConfig, type EcsCapacityProvider, type Ec2CapacityConfig, type QueueScalingConfig } from "../../resources/aws/compute/ecs.js";
|
|
11
11
|
import type { EcsServiceAlarmThresholds } 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 };
|
|
15
15
|
export { ScalingType };
|
|
16
|
-
export type { EcsCapacityProvider, Ec2CapacityConfig };
|
|
16
|
+
export type { EcsCapacityProvider, Ec2CapacityConfig, QueueScalingConfig };
|
|
17
17
|
/**
|
|
18
18
|
* Configuration for ECS capacity providers.
|
|
19
19
|
*/
|
|
@@ -450,6 +450,12 @@ export interface EcsScalingConfig {
|
|
|
450
450
|
minCapacity?: number;
|
|
451
451
|
maxCapacity?: number;
|
|
452
452
|
scalingType?: ScalingType;
|
|
453
|
+
/**
|
|
454
|
+
* Queue-depth scaling config. REQUIRED when `scalingType` is
|
|
455
|
+
* `ScalingType.QUEUE`. Lets a `desiredCount: 0` service wake from zero on
|
|
456
|
+
* SQS backlog (the only scaling mode that does).
|
|
457
|
+
*/
|
|
458
|
+
queueScaling?: QueueScalingConfig;
|
|
453
459
|
}
|
|
454
460
|
/**
|
|
455
461
|
* Host-bind volume mounted into one or more containers in the same task.
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { type Environment
|
|
1
|
+
import { type Environment } from "aws-cdk-lib";
|
|
2
|
+
import { type Construct } from "constructs";
|
|
3
|
+
import { Account, type AccountProps } from "./account.js";
|
|
2
4
|
import { type CustomPermissionSets } from "../../config/aws/identityCenter.js";
|
|
3
5
|
import { ScpPreset } from "../../config/aws/scpPreset.js";
|
|
4
6
|
import type { ScpPresetProps } from "../../config/aws/scpPreset.js";
|
|
5
7
|
import { OrganisationResource } from "../../resources/aws/organisation/index.js";
|
|
6
|
-
type ExtendedStackProps = Omit<StackProps, "env"> & {
|
|
7
|
-
env?: Required<Pick<Environment, "region" | "account">> & Partial<Omit<Environment, "region" | "account">>;
|
|
8
|
-
};
|
|
9
8
|
export type AccountsConfig = {
|
|
10
9
|
readonly [key: string]: readonly string[] | string | AccountsConfig;
|
|
11
10
|
};
|
|
12
|
-
export interface OrganisationProps extends
|
|
11
|
+
export interface OrganisationProps extends AccountProps {
|
|
13
12
|
organisationName: string;
|
|
14
13
|
accounts: AccountsConfig;
|
|
15
14
|
orgEmail: string;
|
|
16
15
|
accountIds?: Record<string, string>;
|
|
17
16
|
identityCenter?: boolean;
|
|
18
17
|
allowedRegions?: string[];
|
|
18
|
+
env?: Required<Pick<Environment, "region" | "account">> & Partial<Omit<Environment, "region" | "account">>;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Organisation CDK stack.
|
|
@@ -27,14 +27,26 @@ export interface OrganisationProps extends ExtendedStackProps {
|
|
|
27
27
|
* - Identity Centre configuration
|
|
28
28
|
* - Cost allocation tag auto-activation (daily Lambda)
|
|
29
29
|
*/
|
|
30
|
-
export declare class Organisation extends
|
|
30
|
+
export declare class Organisation extends Account {
|
|
31
31
|
readonly organisationType: "organisation";
|
|
32
32
|
private org;
|
|
33
33
|
private accountRefs;
|
|
34
34
|
private accountMap;
|
|
35
35
|
private accountsConfig;
|
|
36
36
|
private identityCenter?;
|
|
37
|
-
constructor(id: string, props: OrganisationProps);
|
|
37
|
+
constructor(scope: Construct, id: string, props: OrganisationProps);
|
|
38
|
+
/**
|
|
39
|
+
* The organisation root's OIDC deploy connector is owned by the customer-run
|
|
40
|
+
* Quick-Create CloudFormation stack, never the inherited Account connector.
|
|
41
|
+
* Returning `false` is also the short-circuit first clause of Account's OIDC
|
|
42
|
+
* gate, so no `OidcConnector` is synthesised on the root stack.
|
|
43
|
+
*/
|
|
44
|
+
protected receivesDeployRole(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The organisation root runs no application workloads, so it needs no
|
|
47
|
+
* default-ECR-image helper.
|
|
48
|
+
*/
|
|
49
|
+
protected createsEcrDefaultImage(): boolean;
|
|
38
50
|
private createAccountReferences;
|
|
39
51
|
private setupOrganisationFeatures;
|
|
40
52
|
private setupCostAllocationTagActivator;
|
|
@@ -45,4 +57,3 @@ export declare class Organisation extends Stack {
|
|
|
45
57
|
getAccounts(): Record<string, string>;
|
|
46
58
|
enableScps(props: ScpPresetProps): ScpPreset;
|
|
47
59
|
}
|
|
48
|
-
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Stack } from "aws-cdk-lib";
|
|
2
1
|
import App from "../../app.js";
|
|
2
|
+
import { Account } from "./account.js";
|
|
3
3
|
import { IdentityCenter } from "../../config/aws/identityCenter.js";
|
|
4
4
|
import { ScpPreset } from "../../config/aws/scpPreset.js";
|
|
5
5
|
import { OrganisationResource, OrganisationAccount, CostAllocationTagActivator } from "../../resources/aws/organisation/index.js";
|
|
@@ -17,23 +17,20 @@ import { extractAccountNames } from "../../utils/accountsUtils.js";
|
|
|
17
17
|
* - Identity Centre configuration
|
|
18
18
|
* - Cost allocation tag auto-activation (daily Lambda)
|
|
19
19
|
*/
|
|
20
|
-
export class Organisation extends
|
|
20
|
+
export class Organisation extends Account {
|
|
21
21
|
organisationType = "organisation";
|
|
22
22
|
org;
|
|
23
23
|
accountRefs = [];
|
|
24
24
|
accountMap;
|
|
25
25
|
accountsConfig;
|
|
26
26
|
identityCenter;
|
|
27
|
-
constructor(id, props) {
|
|
27
|
+
constructor(scope, id, props) {
|
|
28
28
|
const config = getConfig();
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
account
|
|
32
|
-
};
|
|
33
|
-
if (!env.account) {
|
|
34
|
-
throw new Error("Organisation requires an account ID. Provide it via env.account or ensure CDK context includes accountId.");
|
|
29
|
+
const accountId = props.accountId ?? props.env?.account ?? config.accountId;
|
|
30
|
+
if (!accountId) {
|
|
31
|
+
throw new Error("Organisation requires an account ID. Provide it via env.account, accountId, or ensure CDK context includes accountId.");
|
|
35
32
|
}
|
|
36
|
-
super(
|
|
33
|
+
super(scope, id, { ...props, accountId });
|
|
37
34
|
// Normalise account map keys to lowercase for case-insensitive lookups.
|
|
38
35
|
// providerAccounts stores names as lowercase; ACCOUNTS config uses PascalCase.
|
|
39
36
|
const rawMap = props.accountIds ?? config.accountIds ?? {};
|
|
@@ -57,6 +54,22 @@ export class Organisation extends Stack {
|
|
|
57
54
|
this.createAccountReferences(props);
|
|
58
55
|
this.setupOrganisationFeatures(props.identityCenter ?? true, managementAccountId);
|
|
59
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The organisation root's OIDC deploy connector is owned by the customer-run
|
|
59
|
+
* Quick-Create CloudFormation stack, never the inherited Account connector.
|
|
60
|
+
* Returning `false` is also the short-circuit first clause of Account's OIDC
|
|
61
|
+
* gate, so no `OidcConnector` is synthesised on the root stack.
|
|
62
|
+
*/
|
|
63
|
+
receivesDeployRole() {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The organisation root runs no application workloads, so it needs no
|
|
68
|
+
* default-ECR-image helper.
|
|
69
|
+
*/
|
|
70
|
+
createsEcrDefaultImage() {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
60
73
|
createAccountReferences(props) {
|
|
61
74
|
const allNames = extractAccountNames(props.accounts);
|
|
62
75
|
for (const accountName of allNames) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import App from "../../app.js";
|
|
1
2
|
import { Organisation } from "./organisation.js";
|
|
2
3
|
import { Platform } from "./platform.js";
|
|
3
4
|
import { Account } from "./account.js";
|
|
@@ -5,7 +6,7 @@ export class OrganisationFactory {
|
|
|
5
6
|
static build(id, props) {
|
|
6
7
|
switch (props.type) {
|
|
7
8
|
case "organisation":
|
|
8
|
-
return new Organisation(id, props);
|
|
9
|
+
return new Organisation(App.getInstance(), id, props);
|
|
9
10
|
case "platform":
|
|
10
11
|
return (scope) => new Platform(scope, id, props);
|
|
11
12
|
case "account":
|
|
@@ -10,5 +10,6 @@ export interface PlatformProps extends AccountProps {
|
|
|
10
10
|
export declare class Platform extends Account {
|
|
11
11
|
readonly organisationType: OrganisationType;
|
|
12
12
|
constructor(scope: Construct, id: string, props: PlatformProps);
|
|
13
|
+
protected receivesDeployRole(): boolean;
|
|
13
14
|
enableSecurityServicesAdmin(props: SecurityServicesAdminProps): SecurityServicesAdmin;
|
|
14
15
|
}
|
|
@@ -23,6 +23,9 @@ export class Platform extends Account {
|
|
|
23
23
|
ipamScope: ipam.privateDefaultScopeId
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
+
receivesDeployRole() {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
26
29
|
enableSecurityServicesAdmin(props) {
|
|
27
30
|
return new SecurityServicesAdmin(this, "SecurityServicesAdmin", props);
|
|
28
31
|
}
|
|
@@ -8,19 +8,21 @@ export class BackupVault extends Construct {
|
|
|
8
8
|
vaultName;
|
|
9
9
|
constructor(scope, id, props) {
|
|
10
10
|
super(scope, id);
|
|
11
|
+
// RETAIN default: a DESTROY-policy vault is silently deleted with the stack.
|
|
12
|
+
// Callers opt into DESTROY explicitly for ephemeral/dev vaults.
|
|
13
|
+
const removalPolicy = props.removalPolicy ?? RemovalPolicy.RETAIN;
|
|
11
14
|
const encryptionKey = props.encryptionKey ||
|
|
12
15
|
new CustomerManagedKey(this, `${props.vaultName}Key`, {
|
|
13
16
|
description: `Encryption key for backup vault ${props.vaultName}`,
|
|
14
17
|
aliasName: `cmk/backupVault/${props.vaultName}`,
|
|
15
|
-
removalPolicy
|
|
18
|
+
removalPolicy
|
|
16
19
|
});
|
|
17
20
|
this.vault = new Vault(this, `${props.vaultName}Vault`, {
|
|
18
21
|
backupVaultName: props.vaultName,
|
|
19
22
|
encryptionKey: encryptionKey.key,
|
|
20
23
|
accessPolicy: props.accessPolicy,
|
|
21
24
|
lockConfiguration: props.lockConfiguration,
|
|
22
|
-
|
|
23
|
-
removalPolicy: props.removalPolicy || RemovalPolicy.DESTROY
|
|
25
|
+
removalPolicy
|
|
24
26
|
});
|
|
25
27
|
this.vaultArn = new CfnOutput(this, `${props.vaultName}VaultArn`, {
|
|
26
28
|
key: `${props.vaultName}VaultArn`,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AmiHardwareType } from "aws-cdk-lib/aws-ecs";
|
|
2
2
|
export declare const DEFAULT_EC2_INSTANCE_TYPE = "t4g.micro";
|
|
3
3
|
export declare const DEFAULT_WARM_POOL_MIN_SIZE = 1;
|
|
4
|
-
export declare const DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN =
|
|
4
|
+
export declare const DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN = false;
|
|
5
5
|
export declare const DEFAULT_LOG_RETENTION_DAYS = 14;
|
|
6
6
|
export declare const DEFAULT_FARGATE_CPU = 256;
|
|
7
7
|
export declare const DEFAULT_FARGATE_MEMORY_MIB = 512;
|
|
@@ -2,7 +2,10 @@ import { AmiHardwareType } from "aws-cdk-lib/aws-ecs";
|
|
|
2
2
|
// Canonical source: @fjall/generator schemas/constants.ts — keep in sync
|
|
3
3
|
export const DEFAULT_EC2_INSTANCE_TYPE = "t4g.micro";
|
|
4
4
|
export const DEFAULT_WARM_POOL_MIN_SIZE = 1;
|
|
5
|
-
|
|
5
|
+
// reuseOnScaleIn: true returns scaled-in instances to the warm pool Stopped,
|
|
6
|
+
// which can strand ECS tasks DRAINING indefinitely (the 2026-06-04 outage).
|
|
7
|
+
// Default false — terminate on scale-in. Mirrors the @fjall/generator twin.
|
|
8
|
+
export const DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN = false;
|
|
6
9
|
// 14 days balances cost against retaining enough history for post-mortem debugging
|
|
7
10
|
export const DEFAULT_LOG_RETENTION_DAYS = 14;
|
|
8
11
|
// Smallest valid (cpu, memory) pair on the Fargate matrix — must move together.
|
|
@@ -30,10 +30,10 @@ export class EcsLifecycleHookMigration extends Construct {
|
|
|
30
30
|
const migrationTaskDef = props.migrationTaskDef;
|
|
31
31
|
const targetTaskDefinitionArn = migrationTaskDef?.definitionArn ?? props.taskDefinitionArn;
|
|
32
32
|
const runTaskResources = [
|
|
33
|
-
`arn:
|
|
33
|
+
`arn:${stack.partition}:ecs:${stack.region}:${stack.account}:task-definition/${props.taskDefinitionFamily}:*`
|
|
34
34
|
];
|
|
35
35
|
if (migrationTaskDef !== undefined) {
|
|
36
|
-
runTaskResources.push(`arn:
|
|
36
|
+
runTaskResources.push(`arn:${stack.partition}:ecs:${stack.region}:${stack.account}:task-definition/${migrationTaskDef.family}:*`);
|
|
37
37
|
}
|
|
38
38
|
const passRoleResources = [props.taskExecutionRoleArn, props.taskRoleArn];
|
|
39
39
|
if (migrationTaskDef !== undefined) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Stack } from "aws-cdk-lib";
|
|
2
2
|
import { Effect, Policy, PolicyStatement, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
3
|
-
import {
|
|
3
|
+
import { deriveSsmSecretsPath } from "./ecsTaskDefinition.js";
|
|
4
4
|
/**
|
|
5
5
|
* Creates the execution role for ECS infrastructure operations.
|
|
6
6
|
* Used by the ECS agent to pull images, write logs, and inject secrets.
|
|
@@ -34,18 +34,9 @@ export function createExecutionRole(ctx, serviceName) {
|
|
|
34
34
|
],
|
|
35
35
|
resources: [logGroupArn, `${logGroupArn}:*`]
|
|
36
36
|
}));
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
executionRole.addToPolicy(new PolicyStatement({
|
|
41
|
-
effect: Effect.ALLOW,
|
|
42
|
-
actions: [
|
|
43
|
-
"secretsmanager:GetSecretValue",
|
|
44
|
-
"secretsmanager:DescribeSecret"
|
|
45
|
-
],
|
|
46
|
-
resources: secretArns
|
|
47
|
-
}));
|
|
48
|
-
}
|
|
37
|
+
// Gotcha: do NOT add a manual `secretsImport` grant here. `addContainer({ secrets })`
|
|
38
|
+
// auto-grants `secret.grantRead(executionRole)` on the resolved complete ARN (exact, no
|
|
39
|
+
// wildcard). A manual bare/`-*` statement is the 2026-06-04 outage shape and is dead.
|
|
49
40
|
const serviceProps = ctx.props.services.find((s) => s.name === serviceName);
|
|
50
41
|
const hasSsmSecrets = serviceProps?.containers.some((container) => container.secrets && container.secrets.length > 0) ?? false;
|
|
51
42
|
if (hasSsmSecrets && serviceProps) {
|
|
@@ -1,7 +1,7 @@
|
|
|
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 } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
4
|
+
import { TargetTrackingScalingPolicy, type StepScalingPolicy } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
5
5
|
import { type AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
|
|
6
6
|
import { type EcsServiceProps, type Ec2CapacityConfig } from "./ecsTypes.js";
|
|
7
7
|
import type { EcsConstructContext } from "./ecsContext.js";
|
|
@@ -37,6 +37,8 @@ export declare function getOrCreateAsgCapacityProvider(ctx: EcsConstructContext,
|
|
|
37
37
|
*/
|
|
38
38
|
export declare function createService(ctx: EcsConstructContext, serviceName: string, serviceProps: EcsServiceProps, taskDefinition: FargateTaskDefinition | Ec2TaskDefinition, asgState: AsgCapacityState): FargateService | Ec2Service;
|
|
39
39
|
/**
|
|
40
|
-
* Adds auto-scaling to an ECS service
|
|
40
|
+
* Adds auto-scaling to an ECS service. `CPU`/`MEMORY` emit a target-tracking
|
|
41
|
+
* policy on utilisation; `QUEUE` emits a step-scaling policy on SQS backlog
|
|
42
|
+
* that can wake the service from `desiredCount: 0`.
|
|
41
43
|
*/
|
|
42
|
-
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 | StepScalingPolicy;
|
|
@@ -3,7 +3,8 @@ 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 { PredefinedMetric, ScalableTarget, ServiceNamespace, TargetTrackingScalingPolicy } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
6
|
+
import { AdjustmentType, PredefinedMetric, ScalableTarget, ServiceNamespace, TargetTrackingScalingPolicy } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
7
|
+
import { MathExpression } from "aws-cdk-lib/aws-cloudwatch";
|
|
7
8
|
import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
|
|
8
9
|
import { SecurityGroup } from "../networking/securityGroup.js";
|
|
9
10
|
import { Ec2Instance } from "./ec2.js";
|
|
@@ -282,17 +283,39 @@ export function createService(ctx, serviceName, serviceProps, taskDefinition, as
|
|
|
282
283
|
return service;
|
|
283
284
|
}
|
|
284
285
|
/**
|
|
285
|
-
*
|
|
286
|
+
* Step-scaling intervals on the queue-backlog metric, with
|
|
287
|
+
* `AdjustmentType.EXACT_CAPACITY` (`change` is the absolute DesiredCount).
|
|
288
|
+
* Backlog 0 → 0 tasks (wake-to-zero); ≥1 → 1; ≥100 → 2; ≥500 → 3. Capacities
|
|
289
|
+
* are clamped to the service's `maxCapacity` at build time.
|
|
290
|
+
*/
|
|
291
|
+
const DEFAULT_QUEUE_SCALING_STEPS = [
|
|
292
|
+
{ lower: 0, upper: 1, capacity: 0 },
|
|
293
|
+
{ lower: 1, upper: 100, capacity: 1 },
|
|
294
|
+
{ lower: 100, upper: 500, capacity: 2 },
|
|
295
|
+
{ lower: 500, capacity: 3 }
|
|
296
|
+
];
|
|
297
|
+
const DEFAULT_QUEUE_SCALE_COOLDOWN = Duration.minutes(5);
|
|
298
|
+
/**
|
|
299
|
+
* Adds auto-scaling to an ECS service. `CPU`/`MEMORY` emit a target-tracking
|
|
300
|
+
* policy on utilisation; `QUEUE` emits a step-scaling policy on SQS backlog
|
|
301
|
+
* that can wake the service from `desiredCount: 0`.
|
|
286
302
|
*/
|
|
287
303
|
export function addServiceScaling(ctx, serviceName, serviceProps, service) {
|
|
288
304
|
const desiredCount = serviceProps.desiredCount ?? DEFAULT_DESIRED_COUNT;
|
|
305
|
+
const maxCapacity = serviceProps.maxCapacity ?? Math.max(desiredCount + 1, 3);
|
|
289
306
|
const scalableTarget = new ScalableTarget(ctx.scope, `${serviceName}ScalableTarget`, {
|
|
290
307
|
serviceNamespace: ServiceNamespace.ECS,
|
|
291
308
|
resourceId: `service/${ctx.cluster.clusterName}/${service.serviceName}`,
|
|
292
309
|
scalableDimension: "ecs:service:DesiredCount",
|
|
293
310
|
minCapacity: serviceProps.minCapacity ?? desiredCount,
|
|
294
|
-
maxCapacity
|
|
311
|
+
maxCapacity
|
|
295
312
|
});
|
|
313
|
+
// QUEUE must branch BEFORE the CPU/MEMORY path: it is a mode discriminator
|
|
314
|
+
// with no predefined metric — falling through would silently emit a CPU
|
|
315
|
+
// policy on a service that meant to scale on backlog.
|
|
316
|
+
if (serviceProps.scalingType === ScalingType.QUEUE) {
|
|
317
|
+
return buildQueueScalingPolicy(serviceName, serviceProps.queueScaling, scalableTarget, maxCapacity);
|
|
318
|
+
}
|
|
296
319
|
return new TargetTrackingScalingPolicy(ctx.scope, `${serviceName}ScalingPolicy`, {
|
|
297
320
|
scalingTarget: scalableTarget,
|
|
298
321
|
predefinedMetric: serviceProps.scalingType === ScalingType.MEMORY
|
|
@@ -303,3 +326,53 @@ export function addServiceScaling(ctx, serviceName, serviceProps, service) {
|
|
|
303
326
|
scaleOutCooldown: Duration.seconds(60)
|
|
304
327
|
});
|
|
305
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Builds the step-scaling policy for `ScalingType.QUEUE`. The alarm metric is a
|
|
331
|
+
* `MathExpression` SUM of `ApproximateNumberOfMessagesVisible`
|
|
332
|
+
* (+`…NotVisible` when `includeInFlight`) across the consumed queues, sampled at
|
|
333
|
+
* `Maximum` over 1-minute periods — published independent of running-task count,
|
|
334
|
+
* so the policy fires (and DesiredCount rises from 0) on backlog alone.
|
|
335
|
+
*/
|
|
336
|
+
function buildQueueScalingPolicy(serviceName, config, scalableTarget, maxCapacity) {
|
|
337
|
+
if (config === undefined || config.queues.length === 0) {
|
|
338
|
+
throw new Error(`ECS service '${serviceName}' uses ScalingType.QUEUE but provides no queueScaling.queues — at least one queue is required to compute backlog.`);
|
|
339
|
+
}
|
|
340
|
+
const includeInFlight = config.includeInFlight ?? true;
|
|
341
|
+
const usingMetrics = {};
|
|
342
|
+
const expressionTerms = [];
|
|
343
|
+
config.queues.forEach((queue, index) => {
|
|
344
|
+
const visibleId = `visible${index}`;
|
|
345
|
+
usingMetrics[visibleId] = queue.metricApproximateNumberOfMessagesVisible({
|
|
346
|
+
statistic: "Maximum",
|
|
347
|
+
period: Duration.minutes(1)
|
|
348
|
+
});
|
|
349
|
+
expressionTerms.push(visibleId);
|
|
350
|
+
if (includeInFlight) {
|
|
351
|
+
const inflightId = `inflight${index}`;
|
|
352
|
+
usingMetrics[inflightId] =
|
|
353
|
+
queue.metricApproximateNumberOfMessagesNotVisible({
|
|
354
|
+
statistic: "Maximum",
|
|
355
|
+
period: Duration.minutes(1)
|
|
356
|
+
});
|
|
357
|
+
expressionTerms.push(inflightId);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
const backlogMetric = new MathExpression({
|
|
361
|
+
expression: expressionTerms.join(" + "),
|
|
362
|
+
usingMetrics,
|
|
363
|
+
period: Duration.minutes(1),
|
|
364
|
+
label: `${serviceName}QueueBacklog`
|
|
365
|
+
});
|
|
366
|
+
return scalableTarget.scaleOnMetric(`${serviceName}QueueScaling`, {
|
|
367
|
+
metric: backlogMetric,
|
|
368
|
+
adjustmentType: AdjustmentType.EXACT_CAPACITY,
|
|
369
|
+
scalingSteps: DEFAULT_QUEUE_SCALING_STEPS.map((step) => ({
|
|
370
|
+
lower: step.lower,
|
|
371
|
+
...(step.upper !== undefined && { upper: step.upper }),
|
|
372
|
+
change: Math.min(step.capacity, maxCapacity)
|
|
373
|
+
})),
|
|
374
|
+
cooldown: config.cooldown ?? DEFAULT_QUEUE_SCALE_COOLDOWN,
|
|
375
|
+
evaluationPeriods: 1,
|
|
376
|
+
datapointsToAlarm: 1
|
|
377
|
+
});
|
|
378
|
+
}
|
|
@@ -14,11 +14,6 @@ export declare function getServiceCapacityProvider(serviceProps: EcsServiceProps
|
|
|
14
14
|
export declare function isServiceFargate(serviceProps: EcsServiceProps): boolean;
|
|
15
15
|
/** Checks if a service uses an EC2 capacity provider. */
|
|
16
16
|
export declare function isServiceEc2(serviceProps: EcsServiceProps): boolean;
|
|
17
|
-
/**
|
|
18
|
-
* Collects Secrets Manager secret names from secretsImport for a specific service.
|
|
19
|
-
* Scoped per service to enforce least-privilege on execution roles.
|
|
20
|
-
*/
|
|
21
|
-
export declare function collectSecretsManagerSecretNames(props: EcsClusterProps, serviceName: string): string[];
|
|
22
17
|
/**
|
|
23
18
|
* Derives the SSM secrets path for a service.
|
|
24
19
|
* Uses explicit path if provided, otherwise derives from app/cluster/service names.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { AwsLogDriver, ContainerDependencyCondition, FargateTaskDefinition, Ec2TaskDefinition, NetworkMode, CpuArchitecture, OperatingSystemFamily } from "aws-cdk-lib/aws-ecs";
|
|
2
2
|
import { Duration } from "aws-cdk-lib";
|
|
3
3
|
import { Secret as EcsSecret } from "aws-cdk-lib/aws-ecs";
|
|
4
|
-
import { Secret } from "aws-cdk-lib/aws-secretsmanager";
|
|
5
4
|
import { StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
6
5
|
import { resolveOrgId } from "../../../utils/cdkContext.js";
|
|
7
6
|
import { validateSsmPathComponent } from "./ecsValidation.js";
|
|
8
7
|
import { DEFAULT_LOG_RETENTION_DAYS, DEFAULT_FARGATE_CPU, DEFAULT_FARGATE_MEMORY_MIB, DEFAULT_EC2_CONTAINER_MEMORY_MIB } from "./ecsConstants.js";
|
|
9
8
|
import { getContainerImage } from "./ecsImages.js";
|
|
10
9
|
import { resolveRemoteConnections } from "./ecsRemoteConnections.js";
|
|
10
|
+
import { resolveImportedSecret } from "../secrets/index.js";
|
|
11
11
|
// Re-export extracted functions so existing consumers are not broken
|
|
12
12
|
export { createExecutionRole, createTaskRole } from "./ecsRoles.js";
|
|
13
13
|
export { getContainerImage } from "./ecsImages.js";
|
|
@@ -27,24 +27,6 @@ export function isServiceFargate(serviceProps) {
|
|
|
27
27
|
export function isServiceEc2(serviceProps) {
|
|
28
28
|
return getServiceCapacityProvider(serviceProps) === "EC2";
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Collects Secrets Manager secret names from secretsImport for a specific service.
|
|
32
|
-
* Scoped per service to enforce least-privilege on execution roles.
|
|
33
|
-
*/
|
|
34
|
-
export function collectSecretsManagerSecretNames(props, serviceName) {
|
|
35
|
-
const service = props.services.find((s) => s.name === serviceName);
|
|
36
|
-
if (!service)
|
|
37
|
-
return [];
|
|
38
|
-
const secretNames = new Set();
|
|
39
|
-
for (const container of service.containers) {
|
|
40
|
-
if (container.secretsImport) {
|
|
41
|
-
for (const secretImport of Object.values(container.secretsImport)) {
|
|
42
|
-
secretNames.add(secretImport.name);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return Array.from(secretNames);
|
|
47
|
-
}
|
|
48
30
|
/**
|
|
49
31
|
* Derives the SSM secrets path for a service.
|
|
50
32
|
* Uses explicit path if provided, otherwise derives from app/cluster/service names.
|
|
@@ -148,7 +130,7 @@ export function addContainersToTask(ctx, serviceName, serviceProps, taskDefiniti
|
|
|
148
130
|
const secrets = {};
|
|
149
131
|
if (containerConfig.secretsImport) {
|
|
150
132
|
for (const [key, secretImport] of Object.entries(containerConfig.secretsImport)) {
|
|
151
|
-
const secret =
|
|
133
|
+
const secret = resolveImportedSecret(ctx.scope, `${ctx.props.clusterName}${serviceName}${containerConfig.name}${key}Secret`, secretImport);
|
|
152
134
|
secrets[key] = EcsSecret.fromSecretsManager(secret, secretImport.field);
|
|
153
135
|
}
|
|
154
136
|
}
|
|
@@ -4,7 +4,9 @@ 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 } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
7
|
+
import { type TargetTrackingScalingPolicy, type StepScalingPolicy } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
8
|
+
import { type IQueue } from "aws-cdk-lib/aws-sqs";
|
|
9
|
+
import { type Duration } from "aws-cdk-lib";
|
|
8
10
|
import { type GeoLocation } from "aws-cdk-lib/aws-route53";
|
|
9
11
|
import { type Repository } from "aws-cdk-lib/aws-ecr";
|
|
10
12
|
import { type FargateService, type Ec2Service, type FargateTaskDefinition, type Ec2TaskDefinition } from "aws-cdk-lib/aws-ecs";
|
|
@@ -23,9 +25,42 @@ export declare enum Protocol {
|
|
|
23
25
|
HTTP = 0,
|
|
24
26
|
HTTPS = 1
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Auto-scaling MODE for an ECS service.
|
|
30
|
+
*
|
|
31
|
+
* `CPU`/`MEMORY` double as their CDK `PredefinedMetric` value (target-tracking
|
|
32
|
+
* on utilisation). `QUEUE` is a pure mode discriminator — it has no predefined
|
|
33
|
+
* metric and drives a step-scaling policy on SQS backlog instead (see
|
|
34
|
+
* `addServiceScaling` → `buildQueueScalingPolicy`). Treat this as a scaling-MODE
|
|
35
|
+
* enum, NOT a `PredefinedMetric` alias: a new member must be branched on
|
|
36
|
+
* explicitly, never fall through the CPU/MEMORY utilisation path.
|
|
37
|
+
*/
|
|
26
38
|
export declare enum ScalingType {
|
|
27
39
|
CPU = "ECSServiceAverageCPUUtilization",
|
|
28
|
-
MEMORY = "ECSServiceAverageMemoryUtilization"
|
|
40
|
+
MEMORY = "ECSServiceAverageMemoryUtilization",
|
|
41
|
+
QUEUE = "QueueBacklog"
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Queue-depth (backlog) scaling configuration for `ScalingType.QUEUE`.
|
|
45
|
+
*
|
|
46
|
+
* Drives a step-scaling policy with `AdjustmentType.EXACT_CAPACITY` alarmed on
|
|
47
|
+
* a `MathExpression` SUM of `ApproximateNumberOfMessagesVisible`
|
|
48
|
+
* (+`…NotVisible` when `includeInFlight`) across `queues`. The queue-depth
|
|
49
|
+
* metric publishes every minute independent of running-task count, so the
|
|
50
|
+
* service wakes from `desiredCount: 0` on backlog — unlike CPU/MEMORY
|
|
51
|
+
* target-tracking, which has no datapoint at zero tasks.
|
|
52
|
+
*/
|
|
53
|
+
export interface QueueScalingConfig {
|
|
54
|
+
/** Queues whose combined backlog drives DesiredCount. At least one required. */
|
|
55
|
+
queues: IQueue[];
|
|
56
|
+
/**
|
|
57
|
+
* Include in-flight (received-but-not-deleted) messages in the backlog sum.
|
|
58
|
+
* Default: true — an in-flight message is still work the service is doing,
|
|
59
|
+
* so scaling in to zero while messages are in flight would strand them.
|
|
60
|
+
*/
|
|
61
|
+
includeInFlight?: boolean;
|
|
62
|
+
/** Cooldown after a scale action. Default: 5 minutes. */
|
|
63
|
+
cooldown?: Duration;
|
|
29
64
|
}
|
|
30
65
|
import type { EcsCapacityProvider } from "@fjall/generator";
|
|
31
66
|
export type { EcsCapacityProvider };
|
|
@@ -301,11 +336,12 @@ export interface EcsServiceProps {
|
|
|
301
336
|
/** Desired number of tasks. Default: 2 */
|
|
302
337
|
desiredCount?: number;
|
|
303
338
|
/**
|
|
304
|
-
* Scaling
|
|
305
|
-
* `ScalableTarget` is registered, and `minCapacity`/`maxCapacity` below
|
|
306
|
-
* no effect. The `desiredCount: 0 + minCapacity > 0` validation throw
|
|
307
|
-
* fires regardless, so operator-intent contradictions surface at synth
|
|
308
|
-
* when scaling is disabled.
|
|
339
|
+
* Scaling mode (`CPU`, `MEMORY`, or `QUEUE`). Omit to disable auto-scaling —
|
|
340
|
+
* no `ScalableTarget` is registered, and `minCapacity`/`maxCapacity` below
|
|
341
|
+
* have no effect. The `desiredCount: 0 + minCapacity > 0` validation throw
|
|
342
|
+
* still fires regardless, so operator-intent contradictions surface at synth
|
|
343
|
+
* even when scaling is disabled. `QUEUE` additionally requires `queueScaling`
|
|
344
|
+
* and is the only mode that wakes a `desiredCount: 0` service from zero.
|
|
309
345
|
*/
|
|
310
346
|
scalingType?: ScalingType;
|
|
311
347
|
/**
|
|
@@ -318,6 +354,12 @@ export interface EcsServiceProps {
|
|
|
318
354
|
* Only consulted when `scalingType` is set.
|
|
319
355
|
*/
|
|
320
356
|
maxCapacity?: number;
|
|
357
|
+
/**
|
|
358
|
+
* Queue-depth scaling config. REQUIRED when `scalingType` is
|
|
359
|
+
* `ScalingType.QUEUE`; ignored otherwise. Lets a `desiredCount: 0` service
|
|
360
|
+
* wake from zero on SQS backlog.
|
|
361
|
+
*/
|
|
362
|
+
queueScaling?: QueueScalingConfig;
|
|
321
363
|
/**
|
|
322
364
|
* Routing rules for this service on the cluster's ALB.
|
|
323
365
|
* Required when cluster has multiple services with ports.
|
|
@@ -500,5 +542,5 @@ export interface ServiceData {
|
|
|
500
542
|
containers: ContainerDefinition[];
|
|
501
543
|
primaryContainer?: ContainerDefinition;
|
|
502
544
|
targetGroup?: IApplicationTargetGroup;
|
|
503
|
-
scalingPolicy?: TargetTrackingScalingPolicy;
|
|
545
|
+
scalingPolicy?: TargetTrackingScalingPolicy | StepScalingPolicy;
|
|
504
546
|
}
|
|
@@ -3,8 +3,19 @@ export var Protocol;
|
|
|
3
3
|
Protocol[Protocol["HTTP"] = 0] = "HTTP";
|
|
4
4
|
Protocol[Protocol["HTTPS"] = 1] = "HTTPS";
|
|
5
5
|
})(Protocol || (Protocol = {}));
|
|
6
|
+
/**
|
|
7
|
+
* Auto-scaling MODE for an ECS service.
|
|
8
|
+
*
|
|
9
|
+
* `CPU`/`MEMORY` double as their CDK `PredefinedMetric` value (target-tracking
|
|
10
|
+
* on utilisation). `QUEUE` is a pure mode discriminator — it has no predefined
|
|
11
|
+
* metric and drives a step-scaling policy on SQS backlog instead (see
|
|
12
|
+
* `addServiceScaling` → `buildQueueScalingPolicy`). Treat this as a scaling-MODE
|
|
13
|
+
* enum, NOT a `PredefinedMetric` alias: a new member must be branched on
|
|
14
|
+
* explicitly, never fall through the CPU/MEMORY utilisation path.
|
|
15
|
+
*/
|
|
6
16
|
export var ScalingType;
|
|
7
17
|
(function (ScalingType) {
|
|
8
18
|
ScalingType["CPU"] = "ECSServiceAverageCPUUtilization";
|
|
9
19
|
ScalingType["MEMORY"] = "ECSServiceAverageMemoryUtilization";
|
|
20
|
+
ScalingType["QUEUE"] = "QueueBacklog";
|
|
10
21
|
})(ScalingType || (ScalingType = {}));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NetworkMode } from "aws-cdk-lib/aws-ecs";
|
|
2
|
+
import { ScalingType } from "./ecsTypes.js";
|
|
2
3
|
/**
|
|
3
4
|
* Validates ECS cluster props before construction.
|
|
4
5
|
* Pure function — does not depend on class state.
|
|
@@ -66,6 +67,19 @@ export function validateEcsClusterProps(props) {
|
|
|
66
67
|
`stopTimeout must be an integer between 1 and 120 seconds (got ${container.stopTimeout}).`);
|
|
67
68
|
}
|
|
68
69
|
}
|
|
70
|
+
// A host-bind volume silently no-ops on Fargate (no host filesystem),
|
|
71
|
+
// so a /var/run/docker.sock bind for `docker buildx` would fail at
|
|
72
|
+
// runtime with no synth error. Reject the un-mountable shape at synth.
|
|
73
|
+
if (service.capacityProvider !== "EC2" && container.volumes) {
|
|
74
|
+
const hostBind = container.volumes.find((v) => v.hostSourcePath !== undefined);
|
|
75
|
+
if (hostBind !== undefined) {
|
|
76
|
+
throw new Error(`Service '${service.name}', container '${container.name ?? "(default)"}': ` +
|
|
77
|
+
`volume '${hostBind.name}' sets hostSourcePath ('${hostBind.hostSourcePath}') but the service uses ` +
|
|
78
|
+
`capacityProvider '${service.capacityProvider}'. Host-bind mounts require the EC2 launch type — ` +
|
|
79
|
+
"Fargate has no host filesystem, so the mount is silently dropped at deploy (a /var/run/docker.sock " +
|
|
80
|
+
"bind for in-container Docker builds would never appear). Set capacityProvider: 'EC2' or remove hostSourcePath.");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
69
83
|
}
|
|
70
84
|
if (service.capacityProvider === "EC2" && !service.ec2Config) {
|
|
71
85
|
throw new Error(`Service '${service.name}' uses EC2 capacity provider but no ec2Config is defined. ` +
|
|
@@ -97,6 +111,29 @@ export function validateEcsClusterProps(props) {
|
|
|
97
111
|
"Application Auto Scaling would immediately scale the service back up, defeating the desiredCount: 0 toggle. " +
|
|
98
112
|
"Either set scaling.minCapacity to 0 (placeholder service) or raise desiredCount to match scaling.minCapacity.");
|
|
99
113
|
}
|
|
114
|
+
// A service that starts at zero can only ever come up again on QUEUE
|
|
115
|
+
// backlog scaling — CPU/MEMORY target-tracking produces no datapoint at
|
|
116
|
+
// zero running tasks, so DesiredCount never rises (the 2026-06-04
|
|
117
|
+
// asset-discovery outage). Reject the un-wakeable shape at synth.
|
|
118
|
+
if (service.desiredCount === 0 &&
|
|
119
|
+
service.scalingType !== ScalingType.QUEUE) {
|
|
120
|
+
throw new Error(`Service '${service.name}': desiredCount is 0 but scalingType is ` +
|
|
121
|
+
`${service.scalingType ?? "unset"} — a service that starts at zero can ` +
|
|
122
|
+
"only wake on queue backlog. Use ScalingType.QUEUE with queueScaling.queues " +
|
|
123
|
+
"so it scales up from 0, or raise desiredCount to keep at least one task running. " +
|
|
124
|
+
"CPU/MEMORY target-tracking produces no datapoint at zero tasks and never scales up.");
|
|
125
|
+
}
|
|
126
|
+
if (service.scalingType === ScalingType.QUEUE) {
|
|
127
|
+
if (service.queueScaling === undefined ||
|
|
128
|
+
service.queueScaling.queues.length === 0) {
|
|
129
|
+
throw new Error(`Service '${service.name}': scalingType is ScalingType.QUEUE but provides no ` +
|
|
130
|
+
"queueScaling.queues — at least one queue is required to compute backlog.");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (service.queueScaling !== undefined) {
|
|
134
|
+
throw new Error(`Service '${service.name}': queueScaling is set but scalingType is not ` +
|
|
135
|
+
"ScalingType.QUEUE — queueScaling only applies to the QUEUE scaling mode.");
|
|
136
|
+
}
|
|
100
137
|
if (service.capacityProvider === "EC2" &&
|
|
101
138
|
service.securityGroups !== undefined &&
|
|
102
139
|
service.securityGroups.length > 0) {
|