@fjall/components-infrastructure 3.2.1 → 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/app.d.ts +12 -12
- package/dist/lib/app.js +6 -42
- package/dist/lib/config/aws/scpPreset.js +8 -1
- package/dist/lib/patterns/aws/buildkite/buildkite.js +7 -0
- package/dist/lib/patterns/aws/buildkite/factory.d.ts +27 -0
- package/dist/lib/patterns/aws/buildkite/factory.js +39 -0
- package/dist/lib/patterns/aws/buildkite/iam.js +4 -0
- package/dist/lib/patterns/aws/buildkite/schema.d.ts +6 -79
- package/dist/lib/patterns/aws/buildkite/schema.js +6 -153
- package/dist/lib/patterns/aws/buildkite/userData.d.ts +12 -1
- package/dist/lib/patterns/aws/buildkite/userData.js +14 -3
- package/dist/lib/patterns/aws/buildkite.d.ts +1 -0
- package/dist/lib/patterns/aws/buildkite.js +1 -0
- 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
package/dist/lib/app.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { type RemovalPolicyString } from "./resources/aws/messaging/utils.js";
|
|
|
16
16
|
import { type ServiceRegistrationProps } from "./resources/aws/networking/serviceDiscovery.js";
|
|
17
17
|
import { type IPrivateDnsNamespace, type IService } from "aws-cdk-lib/aws-servicediscovery";
|
|
18
18
|
import { type AnyCompute } from "./patterns/aws/compute.js";
|
|
19
|
-
import { Buildkite, type BuildkitePropsInput } from "./patterns/aws/buildkite.js";
|
|
19
|
+
import { type Buildkite, type BuildkiteFactoryFn, type BuildkitePropsInput } from "./patterns/aws/buildkite.js";
|
|
20
20
|
import { DevSubstrate, type IDevSubstrateProps } from "./patterns/aws/devSubstrate.js";
|
|
21
21
|
import { type Storage, type StorageFactoryFn } from "./patterns/aws/storage.js";
|
|
22
22
|
import { type AnyPattern, type PatternFactoryFn } from "./patterns/aws/pattern.js";
|
|
@@ -288,19 +288,19 @@ export declare class App extends CdkApp {
|
|
|
288
288
|
* same registry posture — Fjall-managed platform infrastructure,
|
|
289
289
|
* deliberately NOT on the customer create/scaffold surface (design § D3).
|
|
290
290
|
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* `props
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* pages to the application; an explicit prop still wins.
|
|
291
|
+
* App-level defaults (cost-allocation environment from `getConfig()`,
|
|
292
|
+
* alarm actions to the account's `SharedAlarmTopicArn` export,
|
|
293
|
+
* `applicationId` to the app name — designs § D10/§ D14) are resolved by
|
|
294
|
+
* `resolveBuildkiteConstructProps`; explicit props always win.
|
|
295
|
+
*
|
|
296
|
+
* The props overload pins construct id `"Buildkite"` (byte-identical
|
|
297
|
+
* logical IDs for pre-factory consumers); the factory overload accepts
|
|
298
|
+
* `BuildkiteFactory.build(id, props)` — the shape the codemod add path
|
|
299
|
+
* emits. Both overloads resolve placement here, so the deploy-core
|
|
300
|
+
* fixed-stack contract cannot be bypassed from an emitted statement.
|
|
302
301
|
*/
|
|
303
302
|
addBuildkite(props: BuildkitePropsInput): Buildkite;
|
|
303
|
+
addBuildkite(fn: BuildkiteFactoryFn): Buildkite;
|
|
304
304
|
/**
|
|
305
305
|
* Add a database resource to the default database stack using the factory pattern.
|
|
306
306
|
* Returns the appropriate database type based on the factory used.
|
package/dist/lib/app.js
CHANGED
|
@@ -9,14 +9,12 @@ import { NetworkFactory } from "./patterns/aws/network.js";
|
|
|
9
9
|
import { MessagingFactory } from "./patterns/aws/messaging.js";
|
|
10
10
|
import { Schedule } from "./resources/aws/messaging/schedule.js";
|
|
11
11
|
import { ServiceDiscoveryNamespace } from "./resources/aws/networking/serviceDiscovery.js";
|
|
12
|
-
import {
|
|
12
|
+
import { BuildkiteFactory } from "./patterns/aws/buildkite.js";
|
|
13
13
|
import { DevSubstrate } from "./patterns/aws/devSubstrate.js";
|
|
14
14
|
import { StandardTagsAspect } from "./utils/standardTagsAspect.js";
|
|
15
15
|
import { BACKUP_TIER_TAG_KEY, BACKUP_TIER_TAG_MAP } from "./utils/backupTierMapping.js";
|
|
16
16
|
import { randomBytes } from "crypto";
|
|
17
17
|
import { getConfig } from "./utils/getConfig.js";
|
|
18
|
-
import { UNKNOWN_ENVIRONMENT } from "./utils/env.js";
|
|
19
|
-
import { SHARED_ALARM_TOPIC_EXPORT_NAME } from "./config/aws/alarmTopic.js";
|
|
20
18
|
import AuditRoleFactory from "./resources/aws/audit/auditRole.js";
|
|
21
19
|
import { FJALL_AUDIT_CONFIG } from "./config/audit.js";
|
|
22
20
|
import { FjallLogger } from "./utils/validationLogger.js";
|
|
@@ -483,46 +481,12 @@ export class App extends CdkApp {
|
|
|
483
481
|
computeStack.addConstruct(substrate);
|
|
484
482
|
return substrate;
|
|
485
483
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
* Same placement contract as `addDevSubstrate` above: deploy-core selects
|
|
491
|
-
* stacks by the fixed six-category enum, so scoping under
|
|
492
|
-
* `${stackPrefix}Compute` is what makes the fleet actually deploy. Also the
|
|
493
|
-
* same registry posture — Fjall-managed platform infrastructure,
|
|
494
|
-
* deliberately NOT on the customer create/scaffold surface (design § D3).
|
|
495
|
-
*
|
|
496
|
-
* Cost-allocation environment resolves from `getConfig()` (design § D10) so
|
|
497
|
-
* the fleet's environment tag matches the deploying config rather than the
|
|
498
|
-
* construct's `management` fallback; an explicit
|
|
499
|
-
* `props.costAllocationEnvironment` still wins.
|
|
500
|
-
*
|
|
501
|
-
* Alarm actions default to the account's `SharedAlarmTopicArn` export
|
|
502
|
-
* (design § D14 — alerting ships WITH the fleet): every fjall-governed
|
|
503
|
-
* account's Account/Platform stack produces that export, so alarms page
|
|
504
|
-
* out of the box. An explicit `props.alarmSnsTopicArn` still wins.
|
|
505
|
-
* `applicationId` defaults to the app name so the alarm webhook can map
|
|
506
|
-
* pages to the application; an explicit prop still wins.
|
|
507
|
-
*/
|
|
508
|
-
addBuildkite(props) {
|
|
484
|
+
addBuildkite(input) {
|
|
485
|
+
const fn = typeof input === "function"
|
|
486
|
+
? input
|
|
487
|
+
: BuildkiteFactory.build("Buildkite", input);
|
|
509
488
|
const computeStack = this.getDefaultComputeStack();
|
|
510
|
-
const
|
|
511
|
-
const constructProps = { ...props, vpc: this.getVpc() };
|
|
512
|
-
// Post-spread injection so a caller-materialised `costAllocationEnvironment:
|
|
513
|
-
// undefined` counts as absent rather than clobbering the resolved value.
|
|
514
|
-
if (constructProps.costAllocationEnvironment === undefined &&
|
|
515
|
-
configEnvironment !== "" &&
|
|
516
|
-
configEnvironment !== UNKNOWN_ENVIRONMENT) {
|
|
517
|
-
constructProps.costAllocationEnvironment = configEnvironment;
|
|
518
|
-
}
|
|
519
|
-
if (constructProps.alarmSnsTopicArn === undefined) {
|
|
520
|
-
constructProps.alarmSnsTopicArn = `import:${SHARED_ALARM_TOPIC_EXPORT_NAME}`;
|
|
521
|
-
}
|
|
522
|
-
if (constructProps.applicationId === undefined) {
|
|
523
|
-
constructProps.applicationId = this.getName();
|
|
524
|
-
}
|
|
525
|
-
const fleet = new Buildkite(computeStack.getStack(), "Buildkite", constructProps);
|
|
489
|
+
const fleet = fn(this, computeStack.getStack());
|
|
526
490
|
computeStack.addConstruct(fleet);
|
|
527
491
|
return fleet;
|
|
528
492
|
}
|
|
@@ -307,7 +307,14 @@ function buildDevIsolationGuardrails() {
|
|
|
307
307
|
Sid: "DenyCrossAccountAssumeRole",
|
|
308
308
|
Effect: "Deny",
|
|
309
309
|
Action: ["sts:AssumeRole", "sts:AssumeRoot"],
|
|
310
|
-
|
|
310
|
+
// NotResource carves delegation roles (the DelegationRole construct's
|
|
311
|
+
// fixed `<label>DelegateHostedZoneRole` naming contract) out of the
|
|
312
|
+
// deny — a delegated child domain deployed into a dev account must
|
|
313
|
+
// assume the parent zone's delegation role cross-account to write its
|
|
314
|
+
// NS. Those roles are NS-write-scoped by construction
|
|
315
|
+
// (delegationRole.ts). AssumeRoot stays denied everywhere: root-user
|
|
316
|
+
// ARNs never match the role pattern.
|
|
317
|
+
NotResource: "arn:aws:iam::*:role/*DelegateHostedZoneRole",
|
|
311
318
|
Condition: {
|
|
312
319
|
StringNotEquals: {
|
|
313
320
|
// IAM policy variable, substituted per-request by AWS — the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CfnOutput, Duration, RemovalPolicy, Stack, Token } from "aws-cdk-lib";
|
|
2
|
+
import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
|
|
2
3
|
import { InstanceArchitecture, InstanceType, MachineImage, SubnetType } from "aws-cdk-lib/aws-ec2";
|
|
3
4
|
import { PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
4
5
|
import { Source } from "aws-cdk-lib/aws-s3-deployment";
|
|
@@ -106,6 +107,12 @@ export class Buildkite extends Construct {
|
|
|
106
107
|
userData,
|
|
107
108
|
role: agentRole,
|
|
108
109
|
ssmSessionPermissions: false,
|
|
110
|
+
// Upstream parity: EnableDetailedMonitoring defaults "false" in the
|
|
111
|
+
// pinned v6.68.1 template, and the wrapper's absent-default is DETAILED
|
|
112
|
+
// — omitting this silently deviated. The fleet's alarms and scaler run
|
|
113
|
+
// on Buildkite queue metrics, not EC2 metrics, so 1-minute granularity
|
|
114
|
+
// buys nothing here.
|
|
115
|
+
instanceMonitoring: Monitoring.BASIC,
|
|
109
116
|
blockDevices: [
|
|
110
117
|
{
|
|
111
118
|
deviceName: "/dev/xvda",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Construct } from "constructs";
|
|
2
|
+
import type App from "../../../app.js";
|
|
3
|
+
import { Buildkite, type BuildkiteConstructProps } from "./buildkite.js";
|
|
4
|
+
import type { BuildkitePropsInput } from "./schema.js";
|
|
5
|
+
/**
|
|
6
|
+
* Thunk returned by `BuildkiteFactory.build` — materialised only by
|
|
7
|
+
* `App.addBuildkite`, which owns stack placement (the fleet has exactly one
|
|
8
|
+
* legal home, `${stackPrefix}Compute`; design
|
|
9
|
+
* `aiDocs/designs/2026-07-18-fjall-add-buildkite.md` § D2).
|
|
10
|
+
*/
|
|
11
|
+
export type BuildkiteFactoryFn = (app: App, scope: Construct) => Buildkite;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve construct props from plain-data input: VPC injection plus the three
|
|
14
|
+
* app-level defaults (cost-allocation environment, shared alarm topic,
|
|
15
|
+
* applicationId). Single source for both `App.addBuildkite` entry paths
|
|
16
|
+
* (design § D2) — explicit props always win.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveBuildkiteConstructProps(app: App, props: BuildkitePropsInput): BuildkiteConstructProps;
|
|
19
|
+
/**
|
|
20
|
+
* Factory for the self-hosted Buildkite agent fleet — the codemod add path
|
|
21
|
+
* emits `app.addBuildkite(BuildkiteFactory.build("Buildkite", { … }))`
|
|
22
|
+
* (design § D2/§ D3). Validation, the region guard, cost tags, and pinned-AMI
|
|
23
|
+
* resolution all live in the `Buildkite` constructor and fire on every path.
|
|
24
|
+
*/
|
|
25
|
+
export declare class BuildkiteFactory {
|
|
26
|
+
static build(id: string, props: BuildkitePropsInput): BuildkiteFactoryFn;
|
|
27
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SHARED_ALARM_TOPIC_EXPORT_NAME } from "../../../config/aws/alarmTopic.js";
|
|
2
|
+
import { UNKNOWN_ENVIRONMENT } from "../../../utils/env.js";
|
|
3
|
+
import { getConfig } from "../../../utils/getConfig.js";
|
|
4
|
+
import { Buildkite } from "./buildkite.js";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve construct props from plain-data input: VPC injection plus the three
|
|
7
|
+
* app-level defaults (cost-allocation environment, shared alarm topic,
|
|
8
|
+
* applicationId). Single source for both `App.addBuildkite` entry paths
|
|
9
|
+
* (design § D2) — explicit props always win.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveBuildkiteConstructProps(app, props) {
|
|
12
|
+
const configEnvironment = getConfig().environment;
|
|
13
|
+
const constructProps = { ...props, vpc: app.getVpc() };
|
|
14
|
+
// Post-spread injection so a caller-materialised `costAllocationEnvironment:
|
|
15
|
+
// undefined` counts as absent rather than clobbering the resolved value.
|
|
16
|
+
if (constructProps.costAllocationEnvironment === undefined &&
|
|
17
|
+
configEnvironment !== "" &&
|
|
18
|
+
configEnvironment !== UNKNOWN_ENVIRONMENT) {
|
|
19
|
+
constructProps.costAllocationEnvironment = configEnvironment;
|
|
20
|
+
}
|
|
21
|
+
if (constructProps.alarmSnsTopicArn === undefined) {
|
|
22
|
+
constructProps.alarmSnsTopicArn = `import:${SHARED_ALARM_TOPIC_EXPORT_NAME}`;
|
|
23
|
+
}
|
|
24
|
+
if (constructProps.applicationId === undefined) {
|
|
25
|
+
constructProps.applicationId = app.getName();
|
|
26
|
+
}
|
|
27
|
+
return constructProps;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Factory for the self-hosted Buildkite agent fleet — the codemod add path
|
|
31
|
+
* emits `app.addBuildkite(BuildkiteFactory.build("Buildkite", { … }))`
|
|
32
|
+
* (design § D2/§ D3). Validation, the region guard, cost tags, and pinned-AMI
|
|
33
|
+
* resolution all live in the `Buildkite` constructor and fire on every path.
|
|
34
|
+
*/
|
|
35
|
+
export class BuildkiteFactory {
|
|
36
|
+
static build(id, props) {
|
|
37
|
+
return (app, scope) => new Buildkite(scope, id, resolveBuildkiteConstructProps(app, props));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -116,6 +116,10 @@ function buildAgentPolicyDocuments(params) {
|
|
|
116
116
|
logging: new PolicyDocument({
|
|
117
117
|
statements: [
|
|
118
118
|
new PolicyStatement({
|
|
119
|
+
// PutRetentionPolicy pairs with ENABLE_EC2_LOG_RETENTION_POLICY
|
|
120
|
+
// ='true' in the install part (userData.ts) — the booting instance
|
|
121
|
+
// applies agentLogRetentionDays to the /buildkite/* groups. Remove
|
|
122
|
+
// only together with that flag.
|
|
119
123
|
actions: [
|
|
120
124
|
"logs:CreateLogGroup",
|
|
121
125
|
"logs:CreateLogStream",
|
|
@@ -1,81 +1,8 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (design §
|
|
7
|
-
*
|
|
8
|
-
* Defaults encode the design's deploy-fleet posture
|
|
9
|
-
* (`aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`):
|
|
10
|
-
* on-demand-only (`spotCapacityPercentage` 0 — ASG mixed-instances has no
|
|
11
|
-
* spot→on-demand fallback, and a spot reclaim mid-deploy is a rollback
|
|
12
|
-
* incident, § D7), scale-to-zero, whole-instance agents, bounded instance
|
|
13
|
-
* staleness, and no ECR / docker-login instance-level credentials (§ D9 —
|
|
14
|
-
* deploy AWS access stays server-minted; the instance profile confers none).
|
|
2
|
+
* Buildkite props schema — re-export barrel. The canonical declaration lives
|
|
3
|
+
* in `@fjall/generator` (`src/schemas/buildkiteSchemas.ts`) so construct
|
|
4
|
+
* validation, the codemod fragment, and the LLM JSON schema all consume one
|
|
5
|
+
* source (design `aiDocs/designs/2026-07-18-fjall-add-buildkite.md` § D4).
|
|
6
|
+
* Do NOT redeclare here — the cross-package reference-identity test pins it.
|
|
15
7
|
*/
|
|
16
|
-
export
|
|
17
|
-
buildkiteQueue: z.ZodString;
|
|
18
|
-
buildkiteOrgSlug: z.ZodString;
|
|
19
|
-
agentTokenSsmParameterName: z.ZodString;
|
|
20
|
-
agentTokenKmsKeyArn: z.ZodOptional<z.ZodString>;
|
|
21
|
-
fjallApiKeySsmParameterName: z.ZodOptional<z.ZodString>;
|
|
22
|
-
instanceType: z.ZodDefault<z.ZodString>;
|
|
23
|
-
agentVolumeSizeGib: z.ZodDefault<z.ZodNumber>;
|
|
24
|
-
agentMinInstances: z.ZodDefault<z.ZodNumber>;
|
|
25
|
-
agentMaxInstances: z.ZodDefault<z.ZodNumber>;
|
|
26
|
-
agentsPerInstance: z.ZodDefault<z.ZodNumber>;
|
|
27
|
-
spotCapacityPercentage: z.ZodDefault<z.ZodNumber>;
|
|
28
|
-
scaleInIdlePeriodSeconds: z.ZodDefault<z.ZodNumber>;
|
|
29
|
-
disconnectAfterUptimeSeconds: z.ZodDefault<z.ZodNumber>;
|
|
30
|
-
maxInstanceLifetimeDays: z.ZodDefault<z.ZodNumber>;
|
|
31
|
-
terminateInstanceAfterJob: z.ZodDefault<z.ZodBoolean>;
|
|
32
|
-
purgeBuildsOnDiskFull: z.ZodDefault<z.ZodBoolean>;
|
|
33
|
-
terminateInstanceOnDiskFull: z.ZodDefault<z.ZodBoolean>;
|
|
34
|
-
logRetentionDays: z.ZodDefault<z.ZodNumber>;
|
|
35
|
-
buildkiteAgentRelease: z.ZodDefault<z.ZodEnum<{
|
|
36
|
-
stable: "stable";
|
|
37
|
-
beta: "beta";
|
|
38
|
-
edge: "edge";
|
|
39
|
-
}>>;
|
|
40
|
-
buildkiteAgentTags: z.ZodDefault<z.ZodString>;
|
|
41
|
-
buildkiteAgentTimestampLines: z.ZodDefault<z.ZodBoolean>;
|
|
42
|
-
buildkiteAgentExperiments: z.ZodDefault<z.ZodString>;
|
|
43
|
-
buildkiteAgentTracingBackend: z.ZodDefault<z.ZodEnum<{
|
|
44
|
-
"": "";
|
|
45
|
-
datadog: "datadog";
|
|
46
|
-
opentelemetry: "opentelemetry";
|
|
47
|
-
}>>;
|
|
48
|
-
buildkiteAgentCancelGracePeriodSeconds: z.ZodDefault<z.ZodNumber>;
|
|
49
|
-
enableSecretsPlugin: z.ZodDefault<z.ZodBoolean>;
|
|
50
|
-
enableEcrPlugin: z.ZodDefault<z.ZodBoolean>;
|
|
51
|
-
enableDockerLoginPlugin: z.ZodDefault<z.ZodBoolean>;
|
|
52
|
-
enableDockerUserNamespaceRemap: z.ZodDefault<z.ZodBoolean>;
|
|
53
|
-
enableDockerExperimental: z.ZodDefault<z.ZodBoolean>;
|
|
54
|
-
dockerNetworkingProtocol: z.ZodDefault<z.ZodEnum<{
|
|
55
|
-
ipv4: "ipv4";
|
|
56
|
-
dualstack: "dualstack";
|
|
57
|
-
}>>;
|
|
58
|
-
enableInstanceStorage: z.ZodDefault<z.ZodBoolean>;
|
|
59
|
-
mountTmpfsAtTmp: z.ZodDefault<z.ZodBoolean>;
|
|
60
|
-
buildkiteAgentEnableGitMirrors: z.ZodDefault<z.ZodBoolean>;
|
|
61
|
-
bootstrapScriptUrl: z.ZodDefault<z.ZodString>;
|
|
62
|
-
agentEnvFileUrl: z.ZodDefault<z.ZodString>;
|
|
63
|
-
scalerEventSchedulePeriod: z.ZodDefault<z.ZodString>;
|
|
64
|
-
scalerMinPollInterval: z.ZodDefault<z.ZodString>;
|
|
65
|
-
scaleOutFactor: z.ZodDefault<z.ZodString>;
|
|
66
|
-
scaleOutWaitingForJobs: z.ZodDefault<z.ZodBoolean>;
|
|
67
|
-
rolePermissionsBoundaryArn: z.ZodOptional<z.ZodString>;
|
|
68
|
-
alarmSnsTopicArn: z.ZodOptional<z.ZodString>;
|
|
69
|
-
applicationId: z.ZodOptional<z.ZodString>;
|
|
70
|
-
costAllocationEnvironment: z.ZodOptional<z.ZodString>;
|
|
71
|
-
costAllocationOwner: z.ZodOptional<z.ZodString>;
|
|
72
|
-
}, z.core.$strict>;
|
|
73
|
-
export type BuildkiteProps = z.infer<typeof BuildkitePropsSchema>;
|
|
74
|
-
/** Caller-facing shape: fields with defaults are optional at the call site. */
|
|
75
|
-
export type BuildkitePropsInput = z.input<typeof BuildkitePropsSchema>;
|
|
76
|
-
/**
|
|
77
|
-
* Validate + default the plain-data props at construct time. Throws a
|
|
78
|
-
* synth-time error listing every violation — the pattern's constructor is the
|
|
79
|
-
* validation boundary, mirroring `ClickHouseDatabase`'s Stage-1 shape.
|
|
80
|
-
*/
|
|
81
|
-
export declare function validateBuildkiteProps(props: BuildkitePropsInput): BuildkiteProps;
|
|
8
|
+
export { BuildkitePropsObjectSchema, BuildkitePropsSchema, validateBuildkiteProps, type BuildkiteProps, type BuildkitePropsInput, type BuildkitePropsObject } from "@fjall/generator";
|
|
@@ -1,155 +1,8 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (design §
|
|
7
|
-
*
|
|
8
|
-
* Defaults encode the design's deploy-fleet posture
|
|
9
|
-
* (`aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`):
|
|
10
|
-
* on-demand-only (`spotCapacityPercentage` 0 — ASG mixed-instances has no
|
|
11
|
-
* spot→on-demand fallback, and a spot reclaim mid-deploy is a rollback
|
|
12
|
-
* incident, § D7), scale-to-zero, whole-instance agents, bounded instance
|
|
13
|
-
* staleness, and no ECR / docker-login instance-level credentials (§ D9 —
|
|
14
|
-
* deploy AWS access stays server-minted; the instance profile confers none).
|
|
2
|
+
* Buildkite props schema — re-export barrel. The canonical declaration lives
|
|
3
|
+
* in `@fjall/generator` (`src/schemas/buildkiteSchemas.ts`) so construct
|
|
4
|
+
* validation, the codemod fragment, and the LLM JSON schema all consume one
|
|
5
|
+
* source (design `aiDocs/designs/2026-07-18-fjall-add-buildkite.md` § D4).
|
|
6
|
+
* Do NOT redeclare here — the cross-package reference-identity test pins it.
|
|
15
7
|
*/
|
|
16
|
-
export
|
|
17
|
-
.object({
|
|
18
|
-
/** Buildkite queue this fleet serves (cluster-scoped via the token). */
|
|
19
|
-
buildkiteQueue: z
|
|
20
|
-
.string()
|
|
21
|
-
.min(1, "buildkiteQueue cannot be empty")
|
|
22
|
-
.max(100)
|
|
23
|
-
.regex(/^[a-zA-Z0-9-_]+$/, "buildkiteQueue must be alphanumeric with hyphens/underscores"),
|
|
24
|
-
/**
|
|
25
|
-
* Buildkite organisation slug (e.g. `fjall-tech`). The
|
|
26
|
-
* buildkite-agent-scaler publishes its CloudWatch metrics dimensioned by
|
|
27
|
-
* {Org, Queue} — the fleet's alarms must query the same pair or they read
|
|
28
|
-
* no data at all (heartbeat permanently ALARM, queued-with-zero-capacity
|
|
29
|
-
* permanently inert).
|
|
30
|
-
*/
|
|
31
|
-
buildkiteOrgSlug: z
|
|
32
|
-
.string()
|
|
33
|
-
.min(1, "buildkiteOrgSlug cannot be empty")
|
|
34
|
-
.max(100)
|
|
35
|
-
.regex(/^[a-z0-9-]+$/, "buildkiteOrgSlug must be a lowercase Buildkite organisation slug"),
|
|
36
|
-
/**
|
|
37
|
-
* Name of the pre-provisioned SSM SecureString holding the cluster-scoped
|
|
38
|
-
* agent token (e.g. `/Buildkite/agents/agent-token`). Provisioned
|
|
39
|
-
* out-of-band via `fjall secrets` tooling — the construct receives the
|
|
40
|
-
* identifier only; no secret value ever transits synth (design § D4).
|
|
41
|
-
*/
|
|
42
|
-
agentTokenSsmParameterName: z
|
|
43
|
-
.string()
|
|
44
|
-
.min(2)
|
|
45
|
-
.regex(/^\//, "agentTokenSsmParameterName must be a full path (leading /)"),
|
|
46
|
-
/**
|
|
47
|
-
* KMS key ARN encrypting the agent-token parameter, when a customer
|
|
48
|
-
* managed key is used. Omit for the AWS-managed `aws/ssm` key.
|
|
49
|
-
*/
|
|
50
|
-
agentTokenKmsKeyArn: z.string().min(1).optional(),
|
|
51
|
-
/**
|
|
52
|
-
* Name of the SSM SecureString holding FJALL_API_KEY. When set, the
|
|
53
|
-
* construct ships a per-job `env` hook into the managed secrets bucket
|
|
54
|
-
* that reads the parameter at job start (rotation takes effect on the
|
|
55
|
-
* next job, no instance replacement — design § D4(iii)) and grants the
|
|
56
|
-
* instance profile read on exactly this parameter.
|
|
57
|
-
*/
|
|
58
|
-
fjallApiKeySsmParameterName: z
|
|
59
|
-
.string()
|
|
60
|
-
.min(2)
|
|
61
|
-
.regex(/^\//, "fjallApiKeySsmParameterName must be a full path (leading /)")
|
|
62
|
-
.optional(),
|
|
63
|
-
/** EC2 instance type. Graviton default per design § D7. */
|
|
64
|
-
instanceType: z.string().min(1).default("c8g.xlarge"),
|
|
65
|
-
agentVolumeSizeGib: z.number().int().min(20).max(1000).default(250),
|
|
66
|
-
agentMinInstances: z.number().int().min(0).default(0),
|
|
67
|
-
agentMaxInstances: z.number().int().min(1).default(2),
|
|
68
|
-
agentsPerInstance: z.number().int().min(1).default(1),
|
|
69
|
-
/**
|
|
70
|
-
* Percentage of capacity on spot. Deploy fleets MUST stay 0 (on-demand
|
|
71
|
-
* only): no spot→on-demand fallback mechanism exists, and a reclaim
|
|
72
|
-
* mid-deploy lands in the rollback-wedge class (design § D7). Non-zero is
|
|
73
|
-
* for Phase-2 CI fleets running idempotent, auto-retried jobs.
|
|
74
|
-
*/
|
|
75
|
-
spotCapacityPercentage: z.number().int().min(0).max(100).default(0),
|
|
76
|
-
/** Agent-driven scale-in: idle seconds before an instance self-terminates. */
|
|
77
|
-
scaleInIdlePeriodSeconds: z.number().int().min(60).default(600),
|
|
78
|
-
/** Bounded instance staleness; also caps stale-token propagation (§ D5). */
|
|
79
|
-
disconnectAfterUptimeSeconds: z.number().int().min(3600).default(86_400),
|
|
80
|
-
maxInstanceLifetimeDays: z.number().int().min(1).max(365).default(7),
|
|
81
|
-
terminateInstanceAfterJob: z.boolean().default(false),
|
|
82
|
-
purgeBuildsOnDiskFull: z.boolean().default(true),
|
|
83
|
-
terminateInstanceOnDiskFull: z.boolean().default(false),
|
|
84
|
-
/** CloudWatch retention for the scaler's log group (days). */
|
|
85
|
-
logRetentionDays: z.number().int().min(1).default(30),
|
|
86
|
-
buildkiteAgentRelease: z.enum(["stable", "beta", "edge"]).default("stable"),
|
|
87
|
-
buildkiteAgentTags: z.string().default(""),
|
|
88
|
-
buildkiteAgentTimestampLines: z.boolean().default(false),
|
|
89
|
-
buildkiteAgentExperiments: z.string().default(""),
|
|
90
|
-
buildkiteAgentTracingBackend: z
|
|
91
|
-
.enum(["", "datadog", "opentelemetry"])
|
|
92
|
-
.default(""),
|
|
93
|
-
buildkiteAgentCancelGracePeriodSeconds: z
|
|
94
|
-
.number()
|
|
95
|
-
.int()
|
|
96
|
-
.min(10)
|
|
97
|
-
.default(60),
|
|
98
|
-
/** S3 secrets-hooks plugin — required for the FJALL_API_KEY env hook. */
|
|
99
|
-
enableSecretsPlugin: z.boolean().default(true),
|
|
100
|
-
/**
|
|
101
|
-
* ECR + docker-login plugins default OFF: the instance profile carries no
|
|
102
|
-
* registry credentials (design § D9); deploy jobs authenticate through
|
|
103
|
-
* server-minted credentials exactly as on hosted agents.
|
|
104
|
-
*/
|
|
105
|
-
enableEcrPlugin: z.boolean().default(false),
|
|
106
|
-
enableDockerLoginPlugin: z.boolean().default(false),
|
|
107
|
-
enableDockerUserNamespaceRemap: z.boolean().default(true),
|
|
108
|
-
enableDockerExperimental: z.boolean().default(false),
|
|
109
|
-
dockerNetworkingProtocol: z.enum(["ipv4", "dualstack"]).default("ipv4"),
|
|
110
|
-
enableInstanceStorage: z.boolean().default(false),
|
|
111
|
-
mountTmpfsAtTmp: z.boolean().default(true),
|
|
112
|
-
buildkiteAgentEnableGitMirrors: z.boolean().default(false),
|
|
113
|
-
bootstrapScriptUrl: z.string().default(""),
|
|
114
|
-
agentEnvFileUrl: z.string().default(""),
|
|
115
|
-
scalerEventSchedulePeriod: z.string().min(1).default("1 minute"),
|
|
116
|
-
scalerMinPollInterval: z.string().min(1).default("10s"),
|
|
117
|
-
scaleOutFactor: z.string().min(1).default("1.0"),
|
|
118
|
-
scaleOutWaitingForJobs: z.boolean().default(false),
|
|
119
|
-
/** IAM permissions boundary for the scaler's roles; emitted only when set. */
|
|
120
|
-
rolePermissionsBoundaryArn: z.string().min(1).optional(),
|
|
121
|
-
/**
|
|
122
|
-
* SNS topic receiving the fleet's two alarms (scaler heartbeat + queued
|
|
123
|
-
* with zero capacity — design § D14). Omit to create the alarms without
|
|
124
|
-
* actions (visible on the console, silent).
|
|
125
|
-
*/
|
|
126
|
-
alarmSnsTopicArn: z.string().min(1).optional(),
|
|
127
|
-
/**
|
|
128
|
-
* Fjall application the fleet belongs to — appended to alarm
|
|
129
|
-
* descriptions and tagged (`fjall:applicationId`) so the alarm webhook
|
|
130
|
-
* can map pages to the application. `App.addBuildkite` injects the app
|
|
131
|
-
* name automatically; set only to override.
|
|
132
|
-
*/
|
|
133
|
-
applicationId: z.string().min(1).optional(),
|
|
134
|
-
costAllocationEnvironment: z.string().min(1).optional(),
|
|
135
|
-
costAllocationOwner: z.string().min(1).optional()
|
|
136
|
-
})
|
|
137
|
-
.strict()
|
|
138
|
-
.refine((props) => props.agentMinInstances <= props.agentMaxInstances, {
|
|
139
|
-
message: "agentMinInstances must be <= agentMaxInstances"
|
|
140
|
-
});
|
|
141
|
-
/**
|
|
142
|
-
* Validate + default the plain-data props at construct time. Throws a
|
|
143
|
-
* synth-time error listing every violation — the pattern's constructor is the
|
|
144
|
-
* validation boundary, mirroring `ClickHouseDatabase`'s Stage-1 shape.
|
|
145
|
-
*/
|
|
146
|
-
export function validateBuildkiteProps(props) {
|
|
147
|
-
const result = BuildkitePropsSchema.safeParse(props);
|
|
148
|
-
if (!result.success) {
|
|
149
|
-
const details = result.error.issues
|
|
150
|
-
.map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`)
|
|
151
|
-
.join("\n");
|
|
152
|
-
throw new Error(`Buildkite: invalid props:\n${details}`);
|
|
153
|
-
}
|
|
154
|
-
return result.data;
|
|
155
|
-
}
|
|
8
|
+
export { BuildkitePropsObjectSchema, BuildkitePropsSchema, validateBuildkiteProps } from "@fjall/generator";
|
|
@@ -42,7 +42,7 @@ export declare function buildBuildkiteDockerCommands(props: BuildkiteProps): str
|
|
|
42
42
|
* `bk-install-elastic-stack.sh`, mirroring the upstream block key-for-key in
|
|
43
43
|
* upstream order (so a fixture-bump diff reads line-by-line). Keys with no
|
|
44
44
|
* schema knob carry the upstream v6.68.1 parameter default verbatim, with
|
|
45
|
-
*
|
|
45
|
+
* four deliberate deviations:
|
|
46
46
|
*
|
|
47
47
|
* - `BUILDKITE_S3_ACL`: our artifact bucket is BucketOwnerEnforced (ACLs
|
|
48
48
|
* disabled), where S3 rejects every canned ACL EXCEPT
|
|
@@ -54,5 +54,16 @@ export declare function buildBuildkiteDockerCommands(props: BuildkiteProps): str
|
|
|
54
54
|
* is out of scope for the Phase-1 deploy fleet; a future signing knob must
|
|
55
55
|
* land WITH the kms:Sign/Verify/GetPublicKey grants it needs (the prior
|
|
56
56
|
* knob shipped without them — a dead toggle that broke agents when set).
|
|
57
|
+
* - `ENABLE_EC2_LOG_RETENTION_POLICY`: hard-pinned `true` (upstream default
|
|
58
|
+
* `false`, "preserve all logs"). Fjall's log-group lifecycle posture is
|
|
59
|
+
* bounded retention everywhere: each booting instance applies
|
|
60
|
+
* `agentLogRetentionDays` to the agent-created `/buildkite/*` groups via
|
|
61
|
+
* the instance role's `logs:PutRetentionPolicy` grant (which this flag
|
|
62
|
+
* makes live — it was stranded while `false`). Retro-applies to existing
|
|
63
|
+
* groups on the next boot after enabling, expiring events older than the
|
|
64
|
+
* window. Residual: runtime-created groups stay untagged — the namespace
|
|
65
|
+
* is account-global (shared across fleets and stacks), so CFN pre-creation
|
|
66
|
+
* would fight cross-stack ownership; cost exposure is bounded by the
|
|
67
|
+
* retention itself.
|
|
57
68
|
*/
|
|
58
69
|
export declare function buildBuildkiteInstallCommands(props: BuildkiteProps, context: BuildkiteUserDataContext): string[];
|
|
@@ -149,7 +149,7 @@ export function buildBuildkiteDockerCommands(props) {
|
|
|
149
149
|
* `bk-install-elastic-stack.sh`, mirroring the upstream block key-for-key in
|
|
150
150
|
* upstream order (so a fixture-bump diff reads line-by-line). Keys with no
|
|
151
151
|
* schema knob carry the upstream v6.68.1 parameter default verbatim, with
|
|
152
|
-
*
|
|
152
|
+
* four deliberate deviations:
|
|
153
153
|
*
|
|
154
154
|
* - `BUILDKITE_S3_ACL`: our artifact bucket is BucketOwnerEnforced (ACLs
|
|
155
155
|
* disabled), where S3 rejects every canned ACL EXCEPT
|
|
@@ -161,6 +161,17 @@ export function buildBuildkiteDockerCommands(props) {
|
|
|
161
161
|
* is out of scope for the Phase-1 deploy fleet; a future signing knob must
|
|
162
162
|
* land WITH the kms:Sign/Verify/GetPublicKey grants it needs (the prior
|
|
163
163
|
* knob shipped without them — a dead toggle that broke agents when set).
|
|
164
|
+
* - `ENABLE_EC2_LOG_RETENTION_POLICY`: hard-pinned `true` (upstream default
|
|
165
|
+
* `false`, "preserve all logs"). Fjall's log-group lifecycle posture is
|
|
166
|
+
* bounded retention everywhere: each booting instance applies
|
|
167
|
+
* `agentLogRetentionDays` to the agent-created `/buildkite/*` groups via
|
|
168
|
+
* the instance role's `logs:PutRetentionPolicy` grant (which this flag
|
|
169
|
+
* makes live — it was stranded while `false`). Retro-applies to existing
|
|
170
|
+
* groups on the next boot after enabling, expiring events older than the
|
|
171
|
+
* window. Residual: runtime-created groups stay untagged — the namespace
|
|
172
|
+
* is account-global (shared across fleets and stacks), so CFN pre-creation
|
|
173
|
+
* would fight cross-stack ownership; cost exposure is bounded by the
|
|
174
|
+
* retention itself.
|
|
164
175
|
*/
|
|
165
176
|
export function buildBuildkiteInstallCommands(props, context) {
|
|
166
177
|
return [
|
|
@@ -219,8 +230,8 @@ export function buildBuildkiteInstallCommands(props, context) {
|
|
|
219
230
|
"RESOURCE_LIMITS_CPU_WEIGHT='100' \\",
|
|
220
231
|
"RESOURCE_LIMITS_CPU_QUOTA='90%' \\",
|
|
221
232
|
"RESOURCE_LIMITS_IO_WEIGHT='80' \\",
|
|
222
|
-
"ENABLE_EC2_LOG_RETENTION_POLICY='
|
|
223
|
-
|
|
233
|
+
"ENABLE_EC2_LOG_RETENTION_POLICY='true' \\",
|
|
234
|
+
`EC2_LOG_RETENTION_DAYS='${props.agentLogRetentionDays}' \\`,
|
|
224
235
|
"/usr/local/bin/bk-install-elastic-stack.sh"
|
|
225
236
|
];
|
|
226
237
|
}
|
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
export { Buildkite, FJALL_ENV_HOOK_OBJECT_KEY, buildFjallApiKeyEnvHookScript, type BuildkiteConstructProps } from "./buildkite/buildkite.js";
|
|
8
8
|
export { BUILDKITE_AMI_OWNER_ACCOUNT_ID, BUILDKITE_CPU_ARCHITECTURES, BUILDKITE_STACK_PINS, resolvePinnedAmiId, resolveScalerSarApplicationArn, type BuildkiteCpuArchitecture } from "./buildkite/pins.js";
|
|
9
9
|
export { BuildkitePropsSchema, validateBuildkiteProps, type BuildkiteProps, type BuildkitePropsInput } from "./buildkite/schema.js";
|
|
10
|
+
export { BuildkiteFactory, type BuildkiteFactoryFn } from "./buildkite/factory.js";
|
|
10
11
|
export { BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST, buildAgentRole, type BuildkiteAgentRoleParams } from "./buildkite/iam.js";
|
|
11
12
|
export { BUILDKITE_DOCKER_PART_ENV_KEYS, BUILDKITE_MOUNT_PART_ENV_KEYS, BUILDKITE_USER_DATA_ENV_KEYS, buildBuildkiteDockerCommands, buildBuildkiteInstallCommands, buildBuildkiteMountCommands, buildBuildkiteUserData, type BuildkiteUserDataContext } from "./buildkite/userData.js";
|
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
export { Buildkite, FJALL_ENV_HOOK_OBJECT_KEY, buildFjallApiKeyEnvHookScript } from "./buildkite/buildkite.js";
|
|
8
8
|
export { BUILDKITE_AMI_OWNER_ACCOUNT_ID, BUILDKITE_CPU_ARCHITECTURES, BUILDKITE_STACK_PINS, resolvePinnedAmiId, resolveScalerSarApplicationArn } from "./buildkite/pins.js";
|
|
9
9
|
export { BuildkitePropsSchema, validateBuildkiteProps } from "./buildkite/schema.js";
|
|
10
|
+
export { BuildkiteFactory } from "./buildkite/factory.js";
|
|
10
11
|
export { BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST, buildAgentRole } from "./buildkite/iam.js";
|
|
11
12
|
export { BUILDKITE_DOCKER_PART_ENV_KEYS, BUILDKITE_MOUNT_PART_ENV_KEYS, BUILDKITE_USER_DATA_ENV_KEYS, buildBuildkiteDockerCommands, buildBuildkiteInstallCommands, buildBuildkiteMountCommands, buildBuildkiteUserData } from "./buildkite/userData.js";
|
|
@@ -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
|
}
|