@fjall/components-infrastructure 3.1.0 → 3.3.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/app.d.ts +12 -5
- package/dist/lib/app.js +6 -28
- package/dist/lib/config/aws/alarmTopic.d.ts +7 -0
- package/dist/lib/config/aws/alarmTopic.js +9 -2
- package/dist/lib/config/aws/scpPreset.js +8 -1
- package/dist/lib/patterns/aws/apexDomainPattern.d.ts +7 -7
- package/dist/lib/patterns/aws/apexDomainPattern.js +10 -14
- package/dist/lib/patterns/aws/buildkite/alarms.d.ts +5 -0
- package/dist/lib/patterns/aws/buildkite/alarms.js +3 -3
- package/dist/lib/patterns/aws/buildkite/buildkite.js +20 -6
- 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 +6 -1
- package/dist/lib/patterns/aws/buildkite/schema.d.ts +6 -78
- package/dist/lib/patterns/aws/buildkite/schema.js +6 -146
- 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 -1
- package/dist/lib/patterns/aws/buildkite.js +1 -1
- package/dist/lib/patterns/aws/devSubstrate.d.ts +26 -15
- package/dist/lib/patterns/aws/devSubstrate.js +47 -27
- package/dist/lib/resources/aws/monitoring/buildkiteAlarms.d.ts +29 -0
- package/dist/lib/resources/aws/monitoring/buildkiteAlarms.js +90 -0
- package/dist/lib/resources/aws/monitoring/index.d.ts +1 -0
- package/dist/lib/resources/aws/monitoring/index.js +1 -0
- package/dist/lib/resources/aws/networking/hostedZone.js +23 -19
- 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,12 +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
|
|
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.
|
|
295
301
|
*/
|
|
296
302
|
addBuildkite(props: BuildkitePropsInput): Buildkite;
|
|
303
|
+
addBuildkite(fn: BuildkiteFactoryFn): Buildkite;
|
|
297
304
|
/**
|
|
298
305
|
* Add a database resource to the default database stack using the factory pattern.
|
|
299
306
|
* Returns the appropriate database type based on the factory used.
|
package/dist/lib/app.js
CHANGED
|
@@ -9,13 +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
18
|
import AuditRoleFactory from "./resources/aws/audit/auditRole.js";
|
|
20
19
|
import { FJALL_AUDIT_CONFIG } from "./config/audit.js";
|
|
21
20
|
import { FjallLogger } from "./utils/validationLogger.js";
|
|
@@ -482,33 +481,12 @@ export class App extends CdkApp {
|
|
|
482
481
|
computeStack.addConstruct(substrate);
|
|
483
482
|
return substrate;
|
|
484
483
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
* Same placement contract as `addDevSubstrate` above: deploy-core selects
|
|
490
|
-
* stacks by the fixed six-category enum, so scoping under
|
|
491
|
-
* `${stackPrefix}Compute` is what makes the fleet actually deploy. Also the
|
|
492
|
-
* same registry posture — Fjall-managed platform infrastructure,
|
|
493
|
-
* deliberately NOT on the customer create/scaffold surface (design § D3).
|
|
494
|
-
*
|
|
495
|
-
* Cost-allocation environment resolves from `getConfig()` (design § D10) so
|
|
496
|
-
* the fleet's environment tag matches the deploying config rather than the
|
|
497
|
-
* construct's `management` fallback; an explicit
|
|
498
|
-
* `props.costAllocationEnvironment` still wins.
|
|
499
|
-
*/
|
|
500
|
-
addBuildkite(props) {
|
|
484
|
+
addBuildkite(input) {
|
|
485
|
+
const fn = typeof input === "function"
|
|
486
|
+
? input
|
|
487
|
+
: BuildkiteFactory.build("Buildkite", input);
|
|
501
488
|
const computeStack = this.getDefaultComputeStack();
|
|
502
|
-
const
|
|
503
|
-
const constructProps = { ...props, vpc: this.getVpc() };
|
|
504
|
-
// Post-spread injection so a caller-materialised `costAllocationEnvironment:
|
|
505
|
-
// undefined` counts as absent rather than clobbering the resolved value.
|
|
506
|
-
if (constructProps.costAllocationEnvironment === undefined &&
|
|
507
|
-
configEnvironment !== "" &&
|
|
508
|
-
configEnvironment !== UNKNOWN_ENVIRONMENT) {
|
|
509
|
-
constructProps.costAllocationEnvironment = configEnvironment;
|
|
510
|
-
}
|
|
511
|
-
const fleet = new Buildkite(computeStack.getStack(), "Buildkite", constructProps);
|
|
489
|
+
const fleet = fn(this, computeStack.getStack());
|
|
512
490
|
computeStack.addConstruct(fleet);
|
|
513
491
|
return fleet;
|
|
514
492
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { CfnOutput } from "aws-cdk-lib";
|
|
2
2
|
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
3
3
|
import { Construct } from "constructs";
|
|
4
|
+
/**
|
|
5
|
+
* The well-known cross-stack export name for the account's shared alarm
|
|
6
|
+
* topic. Consumers build `"import:" + SHARED_ALARM_TOPIC_EXPORT_NAME`
|
|
7
|
+
* strings for `resolveAlertsTopic`; the CfnOutput below is the single
|
|
8
|
+
* producer.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SHARED_ALARM_TOPIC_EXPORT_NAME = "SharedAlarmTopicArn";
|
|
4
11
|
export declare class SharedAlarmTopic extends Construct {
|
|
5
12
|
readonly topic: ITopic;
|
|
6
13
|
readonly topicArn: CfnOutput;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { CfnOutput } from "aws-cdk-lib";
|
|
2
2
|
import { Construct } from "constructs";
|
|
3
3
|
import { SNSTopic } from "../../resources/aws/messaging/sns.js";
|
|
4
|
+
/**
|
|
5
|
+
* The well-known cross-stack export name for the account's shared alarm
|
|
6
|
+
* topic. Consumers build `"import:" + SHARED_ALARM_TOPIC_EXPORT_NAME`
|
|
7
|
+
* strings for `resolveAlertsTopic`; the CfnOutput below is the single
|
|
8
|
+
* producer.
|
|
9
|
+
*/
|
|
10
|
+
export const SHARED_ALARM_TOPIC_EXPORT_NAME = "SharedAlarmTopicArn";
|
|
4
11
|
export class SharedAlarmTopic extends Construct {
|
|
5
12
|
topic;
|
|
6
13
|
topicArn;
|
|
@@ -15,9 +22,9 @@ export class SharedAlarmTopic extends Construct {
|
|
|
15
22
|
// thinking it duplicates the wrapper's AlarmNotificationsTopicArn —
|
|
16
23
|
// the wrapper's auto-output has no exportName and cannot be imported.
|
|
17
24
|
this.topicArn = new CfnOutput(this, "SharedAlarmTopicArn", {
|
|
18
|
-
key:
|
|
25
|
+
key: SHARED_ALARM_TOPIC_EXPORT_NAME,
|
|
19
26
|
value: wrapped.getTopicArn(),
|
|
20
|
-
exportName:
|
|
27
|
+
exportName: SHARED_ALARM_TOPIC_EXPORT_NAME
|
|
21
28
|
});
|
|
22
29
|
}
|
|
23
30
|
}
|
|
@@ -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
|
|
@@ -14,12 +14,12 @@ export interface ApexDomainPatternResult {
|
|
|
14
14
|
*
|
|
15
15
|
* Delegation direction (D8): child writes NS. The apex zone's contribution
|
|
16
16
|
* to delegation is the org-gated `DelegationRole` the `HostedZone` wrapper
|
|
17
|
-
* creates
|
|
18
|
-
* role by LITERAL ARN (`parentDelegationRoleArn`) and UPSERTs
|
|
19
|
-
* records into this zone via `CrossAccountZoneDelegationRecord`.
|
|
20
|
-
* parent-writes `delegations[]` path (NS records from
|
|
21
|
-
* child nameserver exports) was removed with the H13
|
|
22
|
-
* export-locked, child-first-deploy-ordered, and
|
|
23
|
-
* redeploy.
|
|
17
|
+
* creates for created AND adopted zones — the delegated child `Domain`
|
|
18
|
+
* assumes that role by LITERAL ARN (`parentDelegationRoleArn`) and UPSERTs
|
|
19
|
+
* its own NS records into this zone via `CrossAccountZoneDelegationRecord`.
|
|
20
|
+
* The old parent-writes `delegations[]` path (NS records from
|
|
21
|
+
* `Fn.importValue` of child nameserver exports) was removed with the H13
|
|
22
|
+
* legacy layer: it was export-locked, child-first-deploy-ordered, and
|
|
23
|
+
* removal required a parent redeploy.
|
|
24
24
|
*/
|
|
25
25
|
export declare function composeApexDomain(scope: Construct, props: Route53ApexProps): ApexDomainPatternResult;
|
|
@@ -2,32 +2,28 @@ import { HostedZone } from "../../resources/aws/networking/hostedZone.js";
|
|
|
2
2
|
import { DomainCertificate } from "../../resources/aws/networking/domainCertificate.js";
|
|
3
3
|
import { composeTypedDnsRecords } from "./dnsRecordComposer.js";
|
|
4
4
|
import { toPascalCase, getSafeZoneName } from "../../utils/capitaliseString.js";
|
|
5
|
-
import { resolveOrgId } from "../../utils/cdkContext.js";
|
|
6
5
|
/**
|
|
7
6
|
* Composition for `registrar: "route53"`. Creates (or imports) the apex
|
|
8
7
|
* `HostedZone` and composes all user records + certificates.
|
|
9
8
|
*
|
|
10
9
|
* Delegation direction (D8): child writes NS. The apex zone's contribution
|
|
11
10
|
* to delegation is the org-gated `DelegationRole` the `HostedZone` wrapper
|
|
12
|
-
* creates
|
|
13
|
-
* role by LITERAL ARN (`parentDelegationRoleArn`) and UPSERTs
|
|
14
|
-
* records into this zone via `CrossAccountZoneDelegationRecord`.
|
|
15
|
-
* parent-writes `delegations[]` path (NS records from
|
|
16
|
-
* child nameserver exports) was removed with the H13
|
|
17
|
-
* export-locked, child-first-deploy-ordered, and
|
|
18
|
-
* redeploy.
|
|
11
|
+
* creates for created AND adopted zones — the delegated child `Domain`
|
|
12
|
+
* assumes that role by LITERAL ARN (`parentDelegationRoleArn`) and UPSERTs
|
|
13
|
+
* its own NS records into this zone via `CrossAccountZoneDelegationRecord`.
|
|
14
|
+
* The old parent-writes `delegations[]` path (NS records from
|
|
15
|
+
* `Fn.importValue` of child nameserver exports) was removed with the H13
|
|
16
|
+
* legacy layer: it was export-locked, child-first-deploy-ordered, and
|
|
17
|
+
* removal required a parent redeploy.
|
|
19
18
|
*/
|
|
20
19
|
export function composeApexDomain(scope, props) {
|
|
21
20
|
const safeZone = toPascalCase(getSafeZoneName(props.zoneName));
|
|
22
|
-
// Gotcha:
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// this gate.
|
|
26
|
-
const inOrganisation = resolveOrgId(scope.node) !== undefined;
|
|
21
|
+
// Gotcha: `createDelegationRole` is deliberately omitted — the wrapper's
|
|
22
|
+
// org-gated default gives created AND adopted apex zones the DelegationRole;
|
|
23
|
+
// re-adding a create-path-only condition would strand adopted apexes (D8).
|
|
27
24
|
const hostedZoneConstruct = new HostedZone(scope, `${safeZone}HostedZone`, {
|
|
28
25
|
zoneName: props.zoneName,
|
|
29
26
|
hostedZoneId: props.hostedZoneId,
|
|
30
|
-
createDelegationRole: props.hostedZoneId === undefined && inOrganisation,
|
|
31
27
|
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
32
28
|
costAllocationDomain: props.zoneName
|
|
33
29
|
});
|
|
@@ -3,6 +3,11 @@ export interface BuildkiteAlarmParams {
|
|
|
3
3
|
readonly buildkiteOrgSlug: string;
|
|
4
4
|
readonly buildkiteQueue: string;
|
|
5
5
|
readonly autoScalingGroupName: string;
|
|
6
|
+
/**
|
|
7
|
+
* Alarm-action destination, in either `resolveAlertsTopic` string shape:
|
|
8
|
+
* a literal `arn:...` or `"import:<ExportName>"` (e.g.
|
|
9
|
+
* `"import:SharedAlarmTopicArn"`). Omitted → alarms exist but page nobody.
|
|
10
|
+
*/
|
|
6
11
|
readonly alarmSnsTopicArn?: string;
|
|
7
12
|
}
|
|
8
13
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Duration } from "aws-cdk-lib";
|
|
2
2
|
import { Alarm, ComparisonOperator, MathExpression, Metric, TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
|
|
3
3
|
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveAlertsTopic } from "../../../utils/resolveAlertsTopic.js";
|
|
5
5
|
/**
|
|
6
6
|
* Metrics namespace the buildkite-agent-scaler publishes to, dimensioned by
|
|
7
7
|
* {Org, Queue} — BOTH dimensions are required; querying Queue alone reads no
|
|
@@ -70,8 +70,8 @@ export function addBuildkiteAlarms(scope, params) {
|
|
|
70
70
|
evaluationPeriods: 3,
|
|
71
71
|
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
72
72
|
});
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
const topic = resolveAlertsTopic(scope, "BuildkiteAlarmTopic", params.alarmSnsTopicArn);
|
|
74
|
+
if (topic !== undefined) {
|
|
75
75
|
heartbeatAlarm.addAlarmAction(new SnsAction(topic));
|
|
76
76
|
queuedAlarm.addAlarmAction(new SnsAction(topic));
|
|
77
77
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CfnOutput, Duration, Stack, Token } from "aws-cdk-lib";
|
|
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";
|
|
@@ -8,8 +9,9 @@ import { Ec2Instance } from "../../../resources/aws/compute/ec2.js";
|
|
|
8
9
|
import { SamApplication } from "../../../resources/aws/compute/samApplication.js";
|
|
9
10
|
import { Policy } from "../../../resources/aws/iam/index.js";
|
|
10
11
|
import { BucketDeployment, S3Bucket } from "../../../resources/aws/storage/index.js";
|
|
12
|
+
import { createBuildkiteAlarms } from "../../../resources/aws/monitoring/index.js";
|
|
11
13
|
import { applyCostAllocationTags } from "../../../utils/costAllocationTags.js";
|
|
12
|
-
import {
|
|
14
|
+
import { resolveAlertsTopic } from "../../../utils/resolveAlertsTopic.js";
|
|
13
15
|
import { buildAgentRole } from "./iam.js";
|
|
14
16
|
import { BUILDKITE_STACK_PINS, resolvePinnedAmiId, resolveScalerSarApplicationArn } from "./pins.js";
|
|
15
17
|
import { validateBuildkiteProps } from "./schema.js";
|
|
@@ -55,7 +57,10 @@ export class Buildkite extends Construct {
|
|
|
55
57
|
})
|
|
56
58
|
});
|
|
57
59
|
const artifactBucket = new S3Bucket(this, `${id}ArtifactBucket`);
|
|
58
|
-
|
|
60
|
+
// Secrets (SSH deploy keys, git credentials) must survive stack teardown
|
|
61
|
+
// and accidental overwrite; the artifact bucket is scratch output and
|
|
62
|
+
// keeps the wrapper's env-aware default (DESTROY + pre-empty off prod).
|
|
63
|
+
const managedSecretsBucket = new S3Bucket(this, `${id}ManagedSecretsBucket`, { versioned: true, removalPolicy: RemovalPolicy.RETAIN });
|
|
59
64
|
if (config.fjallApiKeySsmParameterName !== undefined) {
|
|
60
65
|
this.addFjallApiKeyEnvHook(id, managedSecretsBucket, config.fjallApiKeySsmParameterName, region);
|
|
61
66
|
}
|
|
@@ -102,6 +107,12 @@ export class Buildkite extends Construct {
|
|
|
102
107
|
userData,
|
|
103
108
|
role: agentRole,
|
|
104
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,
|
|
105
116
|
blockDevices: [
|
|
106
117
|
{
|
|
107
118
|
deviceName: "/dev/xvda",
|
|
@@ -163,12 +174,15 @@ export class Buildkite extends Construct {
|
|
|
163
174
|
costAllocationEnvironment: config.costAllocationEnvironment
|
|
164
175
|
})
|
|
165
176
|
});
|
|
166
|
-
|
|
177
|
+
const alertsTopic = resolveAlertsTopic(this, "BuildkiteAlarmTopic", config.alarmSnsTopicArn);
|
|
178
|
+
createBuildkiteAlarms({
|
|
179
|
+
scope: this,
|
|
167
180
|
buildkiteOrgSlug: config.buildkiteOrgSlug,
|
|
168
181
|
buildkiteQueue: config.buildkiteQueue,
|
|
169
182
|
autoScalingGroupName: autoScalingGroup.autoScalingGroupName,
|
|
170
|
-
...(
|
|
171
|
-
|
|
183
|
+
...(alertsTopic !== undefined && { alarmTopic: alertsTopic }),
|
|
184
|
+
...(config.applicationId !== undefined && {
|
|
185
|
+
applicationId: config.applicationId
|
|
172
186
|
})
|
|
173
187
|
});
|
|
174
188
|
this.artifactBucketName = artifactBucket.bucketName;
|
|
@@ -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
|
+
}
|
|
@@ -58,7 +58,8 @@ export const BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST = [
|
|
|
58
58
|
*/
|
|
59
59
|
export function buildAgentRole(scope, id, params) {
|
|
60
60
|
return new Role(scope, id, {
|
|
61
|
-
|
|
61
|
+
// IAM descriptions only accept [\t\n\r\x20-\x7E\xA1-\xFF] — no em-dashes
|
|
62
|
+
description: "Buildkite agent instance role - allowlist-only, no deploy capability",
|
|
62
63
|
inlinePolicies: buildAgentPolicyDocuments(params),
|
|
63
64
|
assumedBy: new CompositePrincipal(new ServicePrincipal("autoscaling.amazonaws.com"), new ServicePrincipal("ec2.amazonaws.com"))
|
|
64
65
|
});
|
|
@@ -115,6 +116,10 @@ function buildAgentPolicyDocuments(params) {
|
|
|
115
116
|
logging: new PolicyDocument({
|
|
116
117
|
statements: [
|
|
117
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.
|
|
118
123
|
actions: [
|
|
119
124
|
"logs:CreateLogGroup",
|
|
120
125
|
"logs:CreateLogStream",
|
|
@@ -1,80 +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
|
-
costAllocationEnvironment: z.ZodOptional<z.ZodString>;
|
|
70
|
-
costAllocationOwner: z.ZodOptional<z.ZodString>;
|
|
71
|
-
}, z.core.$strict>;
|
|
72
|
-
export type BuildkiteProps = z.infer<typeof BuildkitePropsSchema>;
|
|
73
|
-
/** Caller-facing shape: fields with defaults are optional at the call site. */
|
|
74
|
-
export type BuildkitePropsInput = z.input<typeof BuildkitePropsSchema>;
|
|
75
|
-
/**
|
|
76
|
-
* Validate + default the plain-data props at construct time. Throws a
|
|
77
|
-
* synth-time error listing every violation — the pattern's constructor is the
|
|
78
|
-
* validation boundary, mirroring `ClickHouseDatabase`'s Stage-1 shape.
|
|
79
|
-
*/
|
|
80
|
-
export declare function validateBuildkiteProps(props: BuildkitePropsInput): BuildkiteProps;
|
|
8
|
+
export { BuildkitePropsObjectSchema, BuildkitePropsSchema, validateBuildkiteProps, type BuildkiteProps, type BuildkitePropsInput, type BuildkitePropsObject } from "@fjall/generator";
|
|
@@ -1,148 +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
|
-
costAllocationEnvironment: z.string().min(1).optional(),
|
|
128
|
-
costAllocationOwner: z.string().min(1).optional()
|
|
129
|
-
})
|
|
130
|
-
.strict()
|
|
131
|
-
.refine((props) => props.agentMinInstances <= props.agentMaxInstances, {
|
|
132
|
-
message: "agentMinInstances must be <= agentMaxInstances"
|
|
133
|
-
});
|
|
134
|
-
/**
|
|
135
|
-
* Validate + default the plain-data props at construct time. Throws a
|
|
136
|
-
* synth-time error listing every violation — the pattern's constructor is the
|
|
137
|
-
* validation boundary, mirroring `ClickHouseDatabase`'s Stage-1 shape.
|
|
138
|
-
*/
|
|
139
|
-
export function validateBuildkiteProps(props) {
|
|
140
|
-
const result = BuildkitePropsSchema.safeParse(props);
|
|
141
|
-
if (!result.success) {
|
|
142
|
-
const details = result.error.issues
|
|
143
|
-
.map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`)
|
|
144
|
-
.join("\n");
|
|
145
|
-
throw new Error(`Buildkite: invalid props:\n${details}`);
|
|
146
|
-
}
|
|
147
|
-
return result.data;
|
|
148
|
-
}
|
|
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,6 +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";
|
|
12
|
-
export { addBuildkiteAlarms, type BuildkiteAlarmParams } from "./buildkite/alarms.js";
|
|
@@ -7,6 +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";
|
|
12
|
-
export { addBuildkiteAlarms } from "./buildkite/alarms.js";
|
|
@@ -40,27 +40,38 @@ export interface IDevSubstrateProps {
|
|
|
40
40
|
readonly adoptSlotEcr?: boolean;
|
|
41
41
|
/**
|
|
42
42
|
* Deploy phase for the R2 two-step cert-hang guard. `"zone"` synthesises the
|
|
43
|
-
* `
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
43
|
+
* `devDomain` zone + cross-account NS delegation only; `"full"` additionally
|
|
44
|
+
* builds the wildcard cert + :443 listener (A6) — deploy that only after the
|
|
45
|
+
* delegated NS has propagated, else ACM DNS-validation hangs. Defaults to
|
|
46
|
+
* `"full"`. Inert unless `domain` is set.
|
|
47
47
|
*/
|
|
48
48
|
readonly phase?: DomainDeployPhase;
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* slots via
|
|
50
|
+
* The zone slots serve under — an EXPLICIT name (`devDomain`, e.g.
|
|
51
|
+
* `development.fjall.io`), never derived by prefixing. When omitted, the
|
|
52
|
+
* substrate stands up no zone/cert (early milestone — G2 reaches slots via
|
|
53
|
+
* `curl --resolve`).
|
|
53
54
|
*
|
|
54
|
-
* `
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* zone
|
|
55
|
+
* Adopt mode (`hostedZoneId` present): the substrate adopts an existing zone
|
|
56
|
+
* (e.g. the account's delegated Domain) by import — no zone resource, no
|
|
57
|
+
* delegation record, no NS export. Mutually exclusive with `parentDomain` /
|
|
58
|
+
* `parentDelegationRoleArn`.
|
|
59
|
+
*
|
|
60
|
+
* Create mode (no `hostedZoneId`): the substrate creates the `devDomain`
|
|
61
|
+
* zone. `parentDelegationRoleArn`, when present, wires the same-org **Case-1**
|
|
62
|
+
* in-stack NS delegation (design VD2-a): the child stack's
|
|
63
|
+
* `CrossAccountZoneDelegationRecord` assumes the parent account's
|
|
64
|
+
* `DelegationRole` by LITERAL ARN and UPSERTs the child NS into the
|
|
65
|
+
* `parentDomain` zone — `parentDomain` (a proper DNS suffix of `devDomain`)
|
|
66
|
+
* MUST accompany it. Omit both for a plain externally-owned parent (Case 3 —
|
|
67
|
+
* manual one-off delegation) or a connected different-org parent (Case 2 —
|
|
68
|
+
* worker UPSERT, B3b, deferred): the substrate then emits only the zone + its
|
|
69
|
+
* NS export for out-of-band delegation.
|
|
61
70
|
*/
|
|
62
71
|
readonly domain?: {
|
|
63
|
-
readonly
|
|
72
|
+
readonly devDomain: string;
|
|
73
|
+
readonly hostedZoneId?: string;
|
|
74
|
+
readonly parentDomain?: string;
|
|
64
75
|
readonly parentDelegationRoleArn?: string;
|
|
65
76
|
};
|
|
66
77
|
}
|
|
@@ -4,6 +4,7 @@ import { Cluster } from "aws-cdk-lib/aws-ecs";
|
|
|
4
4
|
import { Repository } from "aws-cdk-lib/aws-ecr";
|
|
5
5
|
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
6
6
|
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
7
|
+
import { PublicHostedZone } from "aws-cdk-lib/aws-route53";
|
|
7
8
|
import { SqsDestination } from "aws-cdk-lib/aws-s3-notifications";
|
|
8
9
|
import { Construct } from "constructs";
|
|
9
10
|
import { readFileSync } from "node:fs";
|
|
@@ -48,7 +49,7 @@ const DEV_SUBSTRATE_WAKER_ID = "DevSubstrateWaker";
|
|
|
48
49
|
const DEV_SUBSTRATE_WAKER_TG_ID = "DevSubstrateWakerTargetGroup";
|
|
49
50
|
const DEV_SUBSTRATE_WAKER_SOURCE_FILE = "devSubstrate.waker.source.cjs";
|
|
50
51
|
const DEV_SUBSTRATE_WAKER_TIMEOUT_SECONDS = 30;
|
|
51
|
-
/** Construct ids for the substrate's
|
|
52
|
+
/** Construct ids for the substrate's `devDomain` slot zone (created or adopted). */
|
|
52
53
|
const DEV_SUBSTRATE_ZONE_ID = "DevSubstrateZone";
|
|
53
54
|
const DEV_SUBSTRATE_DELEGATION_ID = "DevSubstrateDelegation";
|
|
54
55
|
const DEV_SUBSTRATE_PARENT_ROLE_ID = "DevSubstrateParentDelegationRole";
|
|
@@ -306,32 +307,51 @@ export class DevSubstrate extends Construct {
|
|
|
306
307
|
#createDomain(props, loadBalancer) {
|
|
307
308
|
if (props.domain === undefined)
|
|
308
309
|
return {};
|
|
309
|
-
const {
|
|
310
|
-
const childZoneName =
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const parentRole = Role.fromRoleArn(this, DEV_SUBSTRATE_PARENT_ROLE_ID, parentDelegationRoleArn);
|
|
329
|
-
new CrossAccountDelegationRecord(this, DEV_SUBSTRATE_DELEGATION_ID, {
|
|
330
|
-
delegationRole: parentRole,
|
|
331
|
-
delegatedZone: childZone.hostedZone,
|
|
332
|
-
delegatedZoneName: childZoneName,
|
|
333
|
-
parentHostedZoneName: appDomain
|
|
310
|
+
const { devDomain, hostedZoneId, parentDomain, parentDelegationRoleArn } = props.domain;
|
|
311
|
+
const childZoneName = devDomain;
|
|
312
|
+
let childZone;
|
|
313
|
+
if (hostedZoneId !== undefined) {
|
|
314
|
+
// Adopt mode: import only — no zone resource, no delegation record, no NS
|
|
315
|
+
// export (the zone's NS was delegated when the account Domain deployed).
|
|
316
|
+
// Plain CDK import, NOT the HostedZone wrapper: its import path re-emits
|
|
317
|
+
// the global `<zone>-hosted-zone-id` export, colliding with the owning
|
|
318
|
+
// Domain stack's.
|
|
319
|
+
childZone = PublicHostedZone.fromHostedZoneAttributes(this, DEV_SUBSTRATE_ZONE_ID, { hostedZoneId, zoneName: childZoneName });
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// Create mode: createDelegationRole:false because the PARENT account owns
|
|
323
|
+
// the delegation role (this is the child). The HostedZone wrapper's create
|
|
324
|
+
// path auto-emits the `<zone>-nameservers` CFN export the parent consumes
|
|
325
|
+
// to point NS records at us — no manual export needed.
|
|
326
|
+
const createdZone = new HostedZone(this, DEV_SUBSTRATE_ZONE_ID, {
|
|
327
|
+
zoneName: childZoneName,
|
|
328
|
+
createDelegationRole: false
|
|
334
329
|
});
|
|
330
|
+
childZone = createdZone.hostedZone;
|
|
331
|
+
// Case-1 same-org delegation fires only when a parent role ARN is supplied.
|
|
332
|
+
// The in-stack custom resource assumes the parent account's DelegationRole by
|
|
333
|
+
// LITERAL ARN (Role.fromRoleArn) — NOT Fn.importValue, which resolves
|
|
334
|
+
// same-account only and is exactly why the DomainDelegation pattern cannot go
|
|
335
|
+
// cross-account (design VD2-a). The parent DelegationRole trusts the whole
|
|
336
|
+
// org (OrganizationPrincipal), so a sibling dev account can assume it. Absent
|
|
337
|
+
// ARN ⇒ Case 2 (worker UPSERT, B3b) / Case 3 (manual), handled out of band:
|
|
338
|
+
// the zone + NS export above is all the substrate emits.
|
|
339
|
+
if (parentDelegationRoleArn !== undefined) {
|
|
340
|
+
// Re-checked here because direct construct calls bypass the Zod schema.
|
|
341
|
+
if (parentDomain === undefined) {
|
|
342
|
+
throw new Error(`DevSubstrate domain "${devDomain}": parentDelegationRoleArn was ` +
|
|
343
|
+
`supplied without parentDomain. Set domain.parentDomain to the ` +
|
|
344
|
+
`parent zone the DelegationRole belongs to (a proper DNS suffix ` +
|
|
345
|
+
`of devDomain), or omit parentDelegationRoleArn.`);
|
|
346
|
+
}
|
|
347
|
+
const parentRole = Role.fromRoleArn(this, DEV_SUBSTRATE_PARENT_ROLE_ID, parentDelegationRoleArn);
|
|
348
|
+
new CrossAccountDelegationRecord(this, DEV_SUBSTRATE_DELEGATION_ID, {
|
|
349
|
+
delegationRole: parentRole,
|
|
350
|
+
delegatedZone: childZone,
|
|
351
|
+
delegatedZoneName: childZoneName,
|
|
352
|
+
parentHostedZoneName: parentDomain
|
|
353
|
+
});
|
|
354
|
+
}
|
|
335
355
|
}
|
|
336
356
|
// Step 2 of the R2 two-step gate: synthesising the cert before the step-1 NS
|
|
337
357
|
// has propagated hangs ACM DNS-validation for hours, so "zone" stops here.
|
|
@@ -342,7 +362,7 @@ export class DevSubstrate extends Construct {
|
|
|
342
362
|
// would otherwise render an invalid CFN export name.
|
|
343
363
|
const certificate = new DomainCertificate(this, DEV_SUBSTRATE_CERT_ID, {
|
|
344
364
|
domainName: `*.${childZoneName}`,
|
|
345
|
-
hostedZone: childZone
|
|
365
|
+
hostedZone: childZone,
|
|
346
366
|
exportCertificateArn: false
|
|
347
367
|
});
|
|
348
368
|
// Unconditional 404 as on :80 (per-slot rules attach out of band, §5.4). open
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Alarm } from "aws-cdk-lib/aws-cloudwatch";
|
|
2
|
+
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
3
|
+
import type { Construct } from "constructs";
|
|
4
|
+
export interface BuildkiteAlarmsProps {
|
|
5
|
+
scope: Construct;
|
|
6
|
+
buildkiteOrgSlug: string;
|
|
7
|
+
buildkiteQueue: string;
|
|
8
|
+
autoScalingGroupName: string;
|
|
9
|
+
/**
|
|
10
|
+
* Omitted → the alarms exist on the console but page nobody. Unlike the
|
|
11
|
+
* sibling builders the topic is optional: `App.addBuildkite` defaults it to
|
|
12
|
+
* the shared platform topic, and a bare construct must still synth.
|
|
13
|
+
*/
|
|
14
|
+
alarmTopic?: ITopic;
|
|
15
|
+
applicationId?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The fleet's two Phase-1 alarms (design § D14) — the "~zero babysitting"
|
|
19
|
+
* posture is honest only with these:
|
|
20
|
+
*
|
|
21
|
+
* 1. Scaler heartbeat — `ScheduledJobsCount` goes MISSING for 15 minutes.
|
|
22
|
+
* The scaler publishes every poll, so metric absence means the scaler
|
|
23
|
+
* Lambda is dead or failing; `treatMissingData: BREACHING` is the alarm's
|
|
24
|
+
* entire mechanism.
|
|
25
|
+
* 2. Queued with zero capacity — jobs scheduled while the ASG has no
|
|
26
|
+
* in-service instances for 15 minutes: the fleet cannot boot (AMI gone,
|
|
27
|
+
* quota, subnet failure) while work is waiting.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createBuildkiteAlarms(props: BuildkiteAlarmsProps): Alarm[];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Duration } from "aws-cdk-lib";
|
|
2
|
+
import { Alarm, ComparisonOperator, MathExpression, Metric, TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
|
|
3
|
+
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
|
|
4
|
+
import { buildAlarmDescription, registerAlarm, tagAlarmsWithApplicationId } from "./alarmDefaults.js";
|
|
5
|
+
/**
|
|
6
|
+
* Metrics namespace the buildkite-agent-scaler publishes to, dimensioned by
|
|
7
|
+
* {Org, Queue} — BOTH dimensions are required; querying Queue alone reads no
|
|
8
|
+
* data (CloudWatch dimension matching is exact-set), leaving the heartbeat
|
|
9
|
+
* permanently ALARM and the queued alarm permanently inert. Phase-1b rollout
|
|
10
|
+
* gate: verify both metrics carry data after the first build before trusting
|
|
11
|
+
* the alarms (design § D13/D14) — a namespace/dimension mismatch here is
|
|
12
|
+
* invisible to the synth tests.
|
|
13
|
+
*/
|
|
14
|
+
const SCALER_METRICS_NAMESPACE = "Buildkite";
|
|
15
|
+
/**
|
|
16
|
+
* The fleet's two Phase-1 alarms (design § D14) — the "~zero babysitting"
|
|
17
|
+
* posture is honest only with these:
|
|
18
|
+
*
|
|
19
|
+
* 1. Scaler heartbeat — `ScheduledJobsCount` goes MISSING for 15 minutes.
|
|
20
|
+
* The scaler publishes every poll, so metric absence means the scaler
|
|
21
|
+
* Lambda is dead or failing; `treatMissingData: BREACHING` is the alarm's
|
|
22
|
+
* entire mechanism.
|
|
23
|
+
* 2. Queued with zero capacity — jobs scheduled while the ASG has no
|
|
24
|
+
* in-service instances for 15 minutes: the fleet cannot boot (AMI gone,
|
|
25
|
+
* quota, subnet failure) while work is waiting.
|
|
26
|
+
*/
|
|
27
|
+
export function createBuildkiteAlarms(props) {
|
|
28
|
+
const { scope, buildkiteOrgSlug, buildkiteQueue, autoScalingGroupName, alarmTopic, applicationId } = props;
|
|
29
|
+
const alarms = [];
|
|
30
|
+
const snsAction = alarmTopic !== undefined ? new SnsAction(alarmTopic) : undefined;
|
|
31
|
+
const scheduledJobs = new Metric({
|
|
32
|
+
namespace: SCALER_METRICS_NAMESPACE,
|
|
33
|
+
metricName: "ScheduledJobsCount",
|
|
34
|
+
dimensionsMap: {
|
|
35
|
+
Org: buildkiteOrgSlug,
|
|
36
|
+
Queue: buildkiteQueue
|
|
37
|
+
},
|
|
38
|
+
statistic: "Maximum",
|
|
39
|
+
period: Duration.minutes(5)
|
|
40
|
+
});
|
|
41
|
+
const heartbeatAlarm = new Alarm(scope, "ScalerHeartbeatAlarm", {
|
|
42
|
+
alarmDescription: buildAlarmDescription(`Buildkite scaler for queue '${buildkiteQueue}' has stopped ` +
|
|
43
|
+
"publishing metrics — scaler Lambda dead or erroring. Jobs will " +
|
|
44
|
+
"queue with no scale-out.", applicationId),
|
|
45
|
+
metric: scheduledJobs,
|
|
46
|
+
comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD,
|
|
47
|
+
threshold: 0,
|
|
48
|
+
evaluationPeriods: 3,
|
|
49
|
+
treatMissingData: TreatMissingData.BREACHING
|
|
50
|
+
});
|
|
51
|
+
if (snsAction !== undefined) {
|
|
52
|
+
registerAlarm(heartbeatAlarm, snsAction, alarms);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
alarms.push(heartbeatAlarm);
|
|
56
|
+
}
|
|
57
|
+
const inServiceInstances = new Metric({
|
|
58
|
+
namespace: "AWS/AutoScaling",
|
|
59
|
+
metricName: "GroupInServiceInstances",
|
|
60
|
+
dimensionsMap: { AutoScalingGroupName: autoScalingGroupName },
|
|
61
|
+
statistic: "Maximum",
|
|
62
|
+
period: Duration.minutes(5)
|
|
63
|
+
});
|
|
64
|
+
const queuedWithZeroCapacity = new MathExpression({
|
|
65
|
+
expression: "IF(scheduled > 0 AND inService == 0, 1, 0)",
|
|
66
|
+
usingMetrics: {
|
|
67
|
+
scheduled: scheduledJobs,
|
|
68
|
+
inService: inServiceInstances
|
|
69
|
+
},
|
|
70
|
+
period: Duration.minutes(5)
|
|
71
|
+
});
|
|
72
|
+
const queuedAlarm = new Alarm(scope, "QueuedWithZeroCapacityAlarm", {
|
|
73
|
+
alarmDescription: buildAlarmDescription(`Buildkite queue '${buildkiteQueue}' has scheduled jobs but zero ` +
|
|
74
|
+
"in-service agents for 15 minutes — the fleet cannot boot (AMI, " +
|
|
75
|
+
"quota, or subnet failure) while work waits.", applicationId),
|
|
76
|
+
metric: queuedWithZeroCapacity,
|
|
77
|
+
comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
|
|
78
|
+
threshold: 1,
|
|
79
|
+
evaluationPeriods: 3,
|
|
80
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
81
|
+
});
|
|
82
|
+
if (snsAction !== undefined) {
|
|
83
|
+
registerAlarm(queuedAlarm, snsAction, alarms);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
alarms.push(queuedAlarm);
|
|
87
|
+
}
|
|
88
|
+
tagAlarmsWithApplicationId(alarms, applicationId);
|
|
89
|
+
return alarms;
|
|
90
|
+
}
|
|
@@ -4,5 +4,6 @@ export { createRdsAlarms, type RdsAlarmThresholds, type RdsAlarmsProps } from ".
|
|
|
4
4
|
export { createLambdaAlarms, type LambdaAlarmThresholds, type LambdaAlarmsProps } from "./lambdaAlarms.js";
|
|
5
5
|
export { createScheduleAlarms, type ScheduleAlarmThresholds, type CreateScheduleAlarmsProps } from "./scheduleAlarms.js";
|
|
6
6
|
export { createClickHouseAlarms, type ClickHouseAlarmThresholds, type ClickHouseAlarmsProps } from "./clickhouseAlarms.js";
|
|
7
|
+
export { createBuildkiteAlarms, type BuildkiteAlarmsProps } from "./buildkiteAlarms.js";
|
|
7
8
|
export { createLogPatternAlarms, type LogPatternAlarmSpec, type LogPatternAlarmsProps } from "./logPatternAlarms.js";
|
|
8
9
|
export { METRIC_NAMESPACE, type MetricNamespace } from "./metricNamespaces.js";
|
|
@@ -4,5 +4,6 @@ export { createRdsAlarms } from "./rdsAlarms.js";
|
|
|
4
4
|
export { createLambdaAlarms } from "./lambdaAlarms.js";
|
|
5
5
|
export { createScheduleAlarms } from "./scheduleAlarms.js";
|
|
6
6
|
export { createClickHouseAlarms } from "./clickhouseAlarms.js";
|
|
7
|
+
export { createBuildkiteAlarms } from "./buildkiteAlarms.js";
|
|
7
8
|
export { createLogPatternAlarms } from "./logPatternAlarms.js";
|
|
8
9
|
export { METRIC_NAMESPACE } from "./metricNamespaces.js";
|
|
@@ -13,6 +13,9 @@ export class HostedZoneFactory {
|
|
|
13
13
|
return new HostedZone(stack.getStack(), `${safeZone}HostedZone`, {
|
|
14
14
|
hostedZoneId,
|
|
15
15
|
zoneName,
|
|
16
|
+
// BYO app-stack imports never mint the role: its fixed RoleName would
|
|
17
|
+
// collide with the domain stack's own DelegationRole in the same account.
|
|
18
|
+
createDelegationRole: false,
|
|
16
19
|
costAllocationEnvironment: opts?.costAllocationEnvironment,
|
|
17
20
|
costAllocationDomain: opts?.costAllocationDomain,
|
|
18
21
|
description: opts?.description
|
|
@@ -56,25 +59,6 @@ export class HostedZone extends Construct {
|
|
|
56
59
|
value: Fn.join(",", created.hostedZoneNameServers ?? []),
|
|
57
60
|
exportName: exportNames.nameservers
|
|
58
61
|
});
|
|
59
|
-
// Org-gate: a single account has no OrganisationId export, so an org-trusting
|
|
60
|
-
// delegation role would leave an unresolved Fn::ImportValue and roll the stack
|
|
61
|
-
// back. Default follows org presence; explicit `true` opts in (and fails fast).
|
|
62
|
-
const inOrganisation = resolveOrgId(this.node) !== undefined;
|
|
63
|
-
if (props.createDelegationRole === true && !inOrganisation) {
|
|
64
|
-
throw new Error(`HostedZone "${props.zoneName}": createDelegationRole was requested but ` +
|
|
65
|
-
`this account is not part of an AWS Organization (no "orgId" context). ` +
|
|
66
|
-
`Cross-account DNS delegation requires an organisation — omit ` +
|
|
67
|
-
`createDelegationRole for single-account setups, or connect an organisation.`);
|
|
68
|
-
}
|
|
69
|
-
if (props.createDelegationRole ?? inOrganisation) {
|
|
70
|
-
this.delegationRole = new DelegationRole(this, `${safeZone}DelegationRole`, {
|
|
71
|
-
zoneName: props.zoneName,
|
|
72
|
-
hostedZone: created,
|
|
73
|
-
organisationIdExportName: props.organisationIdExportName,
|
|
74
|
-
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
75
|
-
costAllocationDomain: props.costAllocationDomain
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
62
|
}
|
|
79
63
|
else {
|
|
80
64
|
// Import path — `fromHostedZoneAttributes` creates a reference, not a
|
|
@@ -88,6 +72,26 @@ export class HostedZone extends Construct {
|
|
|
88
72
|
this.isImported = true;
|
|
89
73
|
Tags.of(this).add("fjall:description", this.description);
|
|
90
74
|
}
|
|
75
|
+
// Org-gate: a single account has no OrganisationId export, so an org-trusting
|
|
76
|
+
// delegation role would leave an unresolved Fn::ImportValue and roll the stack
|
|
77
|
+
// back. Default follows org presence; explicit `true` opts in (and fails fast).
|
|
78
|
+
// Runs for created AND adopted zones — `grantDelegation` is on IHostedZone.
|
|
79
|
+
const inOrganisation = resolveOrgId(this.node) !== undefined;
|
|
80
|
+
if (props.createDelegationRole === true && !inOrganisation) {
|
|
81
|
+
throw new Error(`HostedZone "${props.zoneName}": createDelegationRole was requested but ` +
|
|
82
|
+
`this account is not part of an AWS Organization (no "orgId" context). ` +
|
|
83
|
+
`Cross-account DNS delegation requires an organisation — omit ` +
|
|
84
|
+
`createDelegationRole for single-account setups, or connect an organisation.`);
|
|
85
|
+
}
|
|
86
|
+
if (props.createDelegationRole ?? inOrganisation) {
|
|
87
|
+
this.delegationRole = new DelegationRole(this, `${safeZone}DelegationRole`, {
|
|
88
|
+
zoneName: props.zoneName,
|
|
89
|
+
hostedZone: this.hostedZone,
|
|
90
|
+
organisationIdExportName: props.organisationIdExportName,
|
|
91
|
+
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
92
|
+
costAllocationDomain: props.costAllocationDomain
|
|
93
|
+
});
|
|
94
|
+
}
|
|
91
95
|
applyCostAllocationTags(this, {
|
|
92
96
|
service: "hostedZone",
|
|
93
97
|
domain: props.costAllocationDomain ?? props.zoneName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
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.3.0",
|
|
71
|
+
"@fjall/util": "^3.3.0",
|
|
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": "cc1e0f520f4b6b5312e16df3751d60ea746dfb7d"
|
|
86
86
|
}
|