@fjall/components-infrastructure 3.0.0 → 3.2.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 +24 -0
- package/dist/lib/app.js +46 -0
- package/dist/lib/config/aws/alarmTopic.d.ts +7 -0
- package/dist/lib/config/aws/alarmTopic.js +9 -2
- 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 +25 -0
- package/dist/lib/patterns/aws/buildkite/alarms.js +78 -0
- package/dist/lib/patterns/aws/buildkite/buildkite.d.ts +46 -0
- package/dist/lib/patterns/aws/buildkite/buildkite.js +240 -0
- package/dist/lib/patterns/aws/buildkite/iam.d.ts +35 -0
- package/dist/lib/patterns/aws/buildkite/iam.js +177 -0
- package/dist/lib/patterns/aws/buildkite/pins.d.ts +69 -0
- package/dist/lib/patterns/aws/buildkite/pins.js +80 -0
- package/dist/lib/patterns/aws/buildkite/schema.d.ts +81 -0
- package/dist/lib/patterns/aws/buildkite/schema.js +155 -0
- package/dist/lib/patterns/aws/buildkite/userData.d.ts +58 -0
- package/dist/lib/patterns/aws/buildkite/userData.js +226 -0
- package/dist/lib/patterns/aws/buildkite.d.ts +11 -59
- package/dist/lib/patterns/aws/buildkite.js +11 -310
- package/dist/lib/patterns/aws/devSubstrate.d.ts +26 -15
- package/dist/lib/patterns/aws/devSubstrate.js +47 -27
- package/dist/lib/resources/aws/compute/ec2.d.ts +15 -0
- package/dist/lib/resources/aws/compute/ec2.js +20 -1
- 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
|
@@ -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
|
|
@@ -122,6 +122,21 @@ export interface Ec2InstanceProps extends StackProps {
|
|
|
122
122
|
* keys or values are rejected by `validateEc2InstanceProps`.
|
|
123
123
|
*/
|
|
124
124
|
tags?: Record<string, string>;
|
|
125
|
+
/**
|
|
126
|
+
* CDK `AutoScalingGroupProps.maxInstanceLifetime` verbatim — ASG replaces
|
|
127
|
+
* instances older than this. CDK enforces the AWS 1–365 day bounds at
|
|
128
|
+
* synth. Absent → no lifetime cap (AWS default).
|
|
129
|
+
*/
|
|
130
|
+
maxInstanceLifetime?: Duration;
|
|
131
|
+
/**
|
|
132
|
+
* CDK `AutoScalingGroupProps.ssmSessionPermissions`. Absent → `true`
|
|
133
|
+
* (existing wrapper behaviour). `true` attaches the
|
|
134
|
+
* `AmazonSSMManagedInstanceCore` managed policy to the instance role —
|
|
135
|
+
* which carries account-wide `ssm:GetParameter`. Least-privilege consumers
|
|
136
|
+
* that supply their own role with scoped Session Manager statements
|
|
137
|
+
* (Buildkite agents) pass `false` and keep the role allowlist-only.
|
|
138
|
+
*/
|
|
139
|
+
ssmSessionPermissions?: boolean;
|
|
125
140
|
}
|
|
126
141
|
export declare class Ec2Instance extends Construct implements IConnectable {
|
|
127
142
|
private launchTemplate;
|
|
@@ -151,6 +151,9 @@ export class Ec2Instance extends Construct {
|
|
|
151
151
|
...(props.capacityRebalance !== undefined && {
|
|
152
152
|
capacityRebalance: props.capacityRebalance
|
|
153
153
|
}),
|
|
154
|
+
...(props.maxInstanceLifetime !== undefined && {
|
|
155
|
+
maxInstanceLifetime: props.maxInstanceLifetime
|
|
156
|
+
}),
|
|
154
157
|
cooldown: Duration.seconds(60),
|
|
155
158
|
groupMetrics: [GroupMetrics.all()],
|
|
156
159
|
...(resolvedUpdatePolicy !== undefined && {
|
|
@@ -165,7 +168,7 @@ export class Ec2Instance extends Construct {
|
|
|
165
168
|
TerminationPolicy.OLDEST_LAUNCH_CONFIGURATION,
|
|
166
169
|
TerminationPolicy.CLOSEST_TO_NEXT_INSTANCE_HOUR
|
|
167
170
|
],
|
|
168
|
-
ssmSessionPermissions: true
|
|
171
|
+
ssmSessionPermissions: props.ssmSessionPermissions ?? true
|
|
169
172
|
};
|
|
170
173
|
// Support spot instances via mixed instances policy
|
|
171
174
|
if (props.spotCapacityPercentage !== undefined &&
|
|
@@ -262,6 +265,22 @@ export class Ec2Instance extends Construct {
|
|
|
262
265
|
}
|
|
263
266
|
}
|
|
264
267
|
function validateEc2InstanceProps(props) {
|
|
268
|
+
if (props.spotCapacityPercentage !== undefined) {
|
|
269
|
+
if (!Number.isInteger(props.spotCapacityPercentage) ||
|
|
270
|
+
props.spotCapacityPercentage < 0 ||
|
|
271
|
+
props.spotCapacityPercentage > 100) {
|
|
272
|
+
throw new Error(`Ec2Instance.spotCapacityPercentage must be an integer between 0 and 100; got ${props.spotCapacityPercentage}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (props.minCapacity !== undefined &&
|
|
276
|
+
(!Number.isInteger(props.minCapacity) || props.minCapacity < 0)) {
|
|
277
|
+
throw new Error(`Ec2Instance.minCapacity must be a non-negative integer; got ${props.minCapacity}`);
|
|
278
|
+
}
|
|
279
|
+
if (props.maxCapacity !== undefined &&
|
|
280
|
+
props.minCapacity !== undefined &&
|
|
281
|
+
props.maxCapacity < props.minCapacity) {
|
|
282
|
+
throw new Error(`Ec2Instance.maxCapacity (${props.maxCapacity}) must be >= minCapacity (${props.minCapacity})`);
|
|
283
|
+
}
|
|
265
284
|
if (props.persistentDataVolume !== undefined) {
|
|
266
285
|
const azs = props.vpcSubnets?.availabilityZones;
|
|
267
286
|
if (azs === undefined || azs.length !== 1) {
|
|
@@ -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.2.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.2.1",
|
|
71
|
+
"@fjall/util": "^3.2.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": "1a249298df7bf3104d9d49b47700234db815ebb1"
|
|
86
86
|
}
|