@fjall/components-infrastructure 2.34.0 → 3.1.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.
Files changed (78) hide show
  1. package/dist/lib/app.d.ts +36 -2
  2. package/dist/lib/app.js +78 -3
  3. package/dist/lib/patterns/aws/apexDomainPattern.d.ts +11 -12
  4. package/dist/lib/patterns/aws/apexDomainPattern.js +11 -31
  5. package/dist/lib/patterns/aws/buildkite/alarms.d.ts +20 -0
  6. package/dist/lib/patterns/aws/buildkite/alarms.js +78 -0
  7. package/dist/lib/patterns/aws/buildkite/buildkite.d.ts +46 -0
  8. package/dist/lib/patterns/aws/buildkite/buildkite.js +233 -0
  9. package/dist/lib/patterns/aws/buildkite/iam.d.ts +35 -0
  10. package/dist/lib/patterns/aws/buildkite/iam.js +176 -0
  11. package/dist/lib/patterns/aws/buildkite/pins.d.ts +69 -0
  12. package/dist/lib/patterns/aws/buildkite/pins.js +80 -0
  13. package/dist/lib/patterns/aws/buildkite/schema.d.ts +80 -0
  14. package/dist/lib/patterns/aws/buildkite/schema.js +148 -0
  15. package/dist/lib/patterns/aws/buildkite/userData.d.ts +58 -0
  16. package/dist/lib/patterns/aws/buildkite/userData.js +226 -0
  17. package/dist/lib/patterns/aws/buildkite.d.ts +12 -59
  18. package/dist/lib/patterns/aws/buildkite.js +12 -310
  19. package/dist/lib/patterns/aws/cdn.js +25 -10
  20. package/dist/lib/patterns/aws/computeEcs.js +6 -1
  21. package/dist/lib/patterns/aws/computeEcsTypes.d.ts +8 -3
  22. package/dist/lib/patterns/aws/delegatedDomainPattern.d.ts +13 -4
  23. package/dist/lib/patterns/aws/delegatedDomainPattern.js +57 -8
  24. package/dist/lib/patterns/aws/devSubstrate.d.ts +2 -1
  25. package/dist/lib/patterns/aws/devSubstrate.js +18 -8
  26. package/dist/lib/patterns/aws/dnsRecordComposer.d.ts +2 -9
  27. package/dist/lib/patterns/aws/dnsRecordComposer.js +5 -97
  28. package/dist/lib/patterns/aws/domain.d.ts +3 -2
  29. package/dist/lib/patterns/aws/domain.js +3 -2
  30. package/dist/lib/patterns/aws/domainValidation.js +71 -46
  31. package/dist/lib/patterns/aws/externalRecordsPattern.js +2 -11
  32. package/dist/lib/patterns/aws/index.d.ts +1 -3
  33. package/dist/lib/patterns/aws/index.js +2 -3
  34. package/dist/lib/patterns/aws/interfaces/domain.d.ts +22 -8
  35. package/dist/lib/patterns/aws/interfaces/index.d.ts +0 -1
  36. package/dist/lib/patterns/aws/interfaces/index.js +3 -0
  37. package/dist/lib/patterns/aws/interfaces/pattern.d.ts +55 -10
  38. package/dist/lib/patterns/aws/patternDomain.d.ts +77 -0
  39. package/dist/lib/patterns/aws/patternDomain.js +198 -0
  40. package/dist/lib/patterns/aws/payload.js +23 -30
  41. package/dist/lib/patterns/aws/staticSite.d.ts +1 -0
  42. package/dist/lib/patterns/aws/staticSite.js +28 -33
  43. package/dist/lib/resources/aws/base/awsStack.d.ts +8 -0
  44. package/dist/lib/resources/aws/base/awsStack.js +20 -17
  45. package/dist/lib/resources/aws/compute/ec2.d.ts +15 -0
  46. package/dist/lib/resources/aws/compute/ec2.js +20 -1
  47. package/dist/lib/resources/aws/compute/ecs.d.ts +1 -1
  48. package/dist/lib/resources/aws/compute/ecs.js +1 -1
  49. package/dist/lib/resources/aws/compute/ecsNetworking.d.ts +1 -1
  50. package/dist/lib/resources/aws/compute/ecsNetworking.js +212 -47
  51. package/dist/lib/resources/aws/compute/ecsTypes.d.ts +60 -9
  52. package/dist/lib/resources/aws/compute/ecsValidation.d.ts +14 -1
  53. package/dist/lib/resources/aws/compute/ecsValidation.js +51 -0
  54. package/dist/lib/resources/aws/networking/dnsRecord/aRecord.js +3 -2
  55. package/dist/lib/resources/aws/networking/dnsRecord/aaaaRecord.js +3 -2
  56. package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.d.ts +23 -1
  57. package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.js +14 -3
  58. package/dist/lib/resources/aws/networking/dnsRecord/caaRecord.js +3 -2
  59. package/dist/lib/resources/aws/networking/dnsRecord/cnameRecord.js +3 -2
  60. package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.d.ts +10 -1
  61. package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.js +21 -4
  62. package/dist/lib/resources/aws/networking/dnsRecord/mxRecord.js +3 -2
  63. package/dist/lib/resources/aws/networking/dnsRecord/nsRecord.js +3 -2
  64. package/dist/lib/resources/aws/networking/dnsRecord/srvRecord.js +3 -2
  65. package/dist/lib/resources/aws/networking/dnsRecord/txtRecord.js +3 -2
  66. package/dist/lib/resources/aws/networking/domainCertificate.d.ts +28 -4
  67. package/dist/lib/resources/aws/networking/domainCertificate.js +24 -10
  68. package/dist/lib/resources/aws/networking/hostedZone.d.ts +11 -0
  69. package/dist/lib/resources/aws/networking/hostedZone.js +6 -0
  70. package/dist/lib/utils/costAllocationTags.d.ts +9 -0
  71. package/dist/lib/utils/costAllocationTags.js +6 -1
  72. package/dist/lib/utils/dnsRecordRegistry.d.ts +52 -0
  73. package/dist/lib/utils/dnsRecordRegistry.js +64 -0
  74. package/dist/lib/utils/domainTypes.d.ts +43 -36
  75. package/dist/lib/utils/domainTypes.js +65 -2
  76. package/dist/lib/utils/managedDomainContext.d.ts +37 -0
  77. package/dist/lib/utils/managedDomainContext.js +111 -0
  78. package/package.json +4 -4
package/dist/lib/app.d.ts CHANGED
@@ -3,7 +3,7 @@ import { type CloudAssembly } from "aws-cdk-lib/cx-api";
3
3
  import { type Construct } from "constructs";
4
4
  import { type IVpc } from "aws-cdk-lib/aws-ec2";
5
5
  import { type Role } from "aws-cdk-lib/aws-iam";
6
- import { AwsStack } from "./resources/index.js";
6
+ import { AwsStack, type AwsStackProps } from "./resources/index.js";
7
7
  import { type Ecr } from "./resources/aws/storage/ecr.js";
8
8
  import { type AnyDatabase } from "./patterns/aws/database.js";
9
9
  import { type INetworkProps, type Network } from "./patterns/aws/network.js";
@@ -16,6 +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
20
  import { DevSubstrate, type IDevSubstrateProps } from "./patterns/aws/devSubstrate.js";
20
21
  import { type Storage, type StorageFactoryFn } from "./patterns/aws/storage.js";
21
22
  import { type AnyPattern, type PatternFactoryFn } from "./patterns/aws/pattern.js";
@@ -123,8 +124,10 @@ export declare class App extends CdkApp {
123
124
  *
124
125
  * @param key - The key of the stack
125
126
  * @param dependencies - The stack(s) that this stack depends on
127
+ * @param props - Stack props applied on first creation only (cached
128
+ * stacks are returned as-is)
126
129
  */
127
- getStack(key: string, dependencies?: AwsStack | AwsStack[]): AwsStack;
130
+ getStack(key: string, dependencies?: AwsStack | AwsStack[], props?: AwsStackProps): AwsStack;
128
131
  /**
129
132
  * Retrieve default compute stack - named as `${this.name}Compute`
130
133
  *
@@ -162,6 +165,21 @@ export declare class App extends CdkApp {
162
165
  * require VPC, but we maintain consistent stack dependency patterns.
163
166
  */
164
167
  getDefaultMessagingStack(): AwsStack;
168
+ /**
169
+ * Retrieve the app-owned us-east-1 certificate stack — named
170
+ * `${this.name}UsEast1Certificates` (domain gold-plating D3, BYO path).
171
+ *
172
+ * CloudFront accepts viewer certificates from us-east-1 only, so
173
+ * CloudFront-consuming patterns (staticsite, payload) whose app deploys to
174
+ * another region provision their DomainCertificate here; the distribution
175
+ * in the Cdn stack consumes the ARN through CDK cross-region references.
176
+ *
177
+ * `crossRegionReferences: true` must be set on BOTH the producing and the
178
+ * consuming stack; both get it at construction (the Cdn stack in
179
+ * getDefaultCdnStack, this stack here) and the emitted
180
+ * Custom::CrossRegionExportWriter/Reader pair is pinned by template test.
181
+ */
182
+ getUsEast1CertificateStack(): AwsStack;
165
183
  /**
166
184
  * Get a VPC by name. If no name is provided, returns the default VPC.
167
185
  *
@@ -260,6 +278,22 @@ export declare class App extends CdkApp {
260
278
  * would synth and hash but never deploy.
261
279
  */
262
280
  addDevSubstrate(props: IDevSubstrateProps): DevSubstrate;
281
+ /**
282
+ * Instantiate the self-hosted Buildkite agent fleet on the default compute
283
+ * stack (design `aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`).
284
+ *
285
+ * Same placement contract as `addDevSubstrate` above: deploy-core selects
286
+ * stacks by the fixed six-category enum, so scoping under
287
+ * `${stackPrefix}Compute` is what makes the fleet actually deploy. Also the
288
+ * same registry posture — Fjall-managed platform infrastructure,
289
+ * deliberately NOT on the customer create/scaffold surface (design § D3).
290
+ *
291
+ * Cost-allocation environment resolves from `getConfig()` (design § D10) so
292
+ * the fleet's environment tag matches the deploying config rather than the
293
+ * construct's `management` fallback; an explicit
294
+ * `props.costAllocationEnvironment` still wins.
295
+ */
296
+ addBuildkite(props: BuildkitePropsInput): Buildkite;
263
297
  /**
264
298
  * Add a database resource to the default database stack using the factory pattern.
265
299
  * Returns the appropriate database type based on the factory used.
package/dist/lib/app.js CHANGED
@@ -9,15 +9,18 @@ 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 { Buildkite } from "./patterns/aws/buildkite.js";
12
13
  import { DevSubstrate } from "./patterns/aws/devSubstrate.js";
13
14
  import { StandardTagsAspect } from "./utils/standardTagsAspect.js";
14
15
  import { BACKUP_TIER_TAG_KEY, BACKUP_TIER_TAG_MAP } from "./utils/backupTierMapping.js";
15
16
  import { randomBytes } from "crypto";
16
17
  import { getConfig } from "./utils/getConfig.js";
18
+ import { UNKNOWN_ENVIRONMENT } from "./utils/env.js";
17
19
  import AuditRoleFactory from "./resources/aws/audit/auditRole.js";
18
20
  import { FJALL_AUDIT_CONFIG } from "./config/audit.js";
19
21
  import { FjallLogger } from "./utils/validationLogger.js";
20
22
  import { getManifestCollector, writeManifest } from "./utils/manifestWriter.js";
23
+ import { resetDnsRecordRegistry } from "./utils/dnsRecordRegistry.js";
21
24
  import { toPascalCase, toKebab } from "./utils/capitaliseString.js";
22
25
  import { COST_ALLOCATION_TAGS } from "./utils/costAllocationTags.js";
23
26
  import { DEFAULT_ORG_ID, resolveOrgId } from "./utils/cdkContext.js";
@@ -168,6 +171,10 @@ export class App extends CdkApp {
168
171
  }
169
172
  static resetForTesting() {
170
173
  App.instance = null;
174
+ // App-scoped synth state resets with the App singleton — the DNS
175
+ // record-collision registry (D5) follows the manifest-collector lifecycle
176
+ // without requiring a third per-test reset call.
177
+ resetDnsRecordRegistry();
171
178
  }
172
179
  /**
173
180
  * Retrieve a stack by key. If the stack does not exist, it will be created.
@@ -175,8 +182,10 @@ export class App extends CdkApp {
175
182
  *
176
183
  * @param key - The key of the stack
177
184
  * @param dependencies - The stack(s) that this stack depends on
185
+ * @param props - Stack props applied on first creation only (cached
186
+ * stacks are returned as-is)
178
187
  */
179
- getStack(key, dependencies) {
188
+ getStack(key, dependencies, props) {
180
189
  // Apply the aspect once before creating the first stack
181
190
  if (!this.aspectApplied &&
182
191
  Object.keys(this.stacks).length === 0 &&
@@ -184,7 +193,7 @@ export class App extends CdkApp {
184
193
  this.applyTagsAspect();
185
194
  }
186
195
  if (!this.stacks[key]) {
187
- this.stacks[key] = new AwsStack(key, dependencies);
196
+ this.stacks[key] = new AwsStack(key, dependencies, props);
188
197
  }
189
198
  return this.stacks[key];
190
199
  }
@@ -225,7 +234,10 @@ export class App extends CdkApp {
225
234
  * stack is synthesised.
226
235
  */
227
236
  getDefaultCdnStack() {
228
- return this.getStack(`${this.stackPrefix}Cdn`, this.networkDisabled ? undefined : this.getDefaultNetworkStack());
237
+ // crossRegionReferences is enabled at creation so the Cdn stack can
238
+ // consume a us-east-1 certificate (domain gold-plating D3, BYO path).
239
+ // The flag is inert unless a cross-region reference is synthesised.
240
+ return this.getStack(`${this.stackPrefix}Cdn`, this.networkDisabled ? undefined : this.getDefaultNetworkStack(), { crossRegionReferences: true });
229
241
  }
230
242
  /**
231
243
  * Retrieve default messaging stack - named as `${this.name}Messaging`
@@ -237,6 +249,39 @@ export class App extends CdkApp {
237
249
  getDefaultMessagingStack() {
238
250
  return this.getStack(`${this.stackPrefix}Messaging`, this.getDefaultNetworkStack());
239
251
  }
252
+ /**
253
+ * Retrieve the app-owned us-east-1 certificate stack — named
254
+ * `${this.name}UsEast1Certificates` (domain gold-plating D3, BYO path).
255
+ *
256
+ * CloudFront accepts viewer certificates from us-east-1 only, so
257
+ * CloudFront-consuming patterns (staticsite, payload) whose app deploys to
258
+ * another region provision their DomainCertificate here; the distribution
259
+ * in the Cdn stack consumes the ARN through CDK cross-region references.
260
+ *
261
+ * `crossRegionReferences: true` must be set on BOTH the producing and the
262
+ * consuming stack; both get it at construction (the Cdn stack in
263
+ * getDefaultCdnStack, this stack here) and the emitted
264
+ * Custom::CrossRegionExportWriter/Reader pair is pinned by template test.
265
+ */
266
+ getUsEast1CertificateStack() {
267
+ const key = `${this.stackPrefix}UsEast1Certificates`;
268
+ if (!this.stacks[key]) {
269
+ const account = process.env.CDK_DEFAULT_ACCOUNT;
270
+ if (!account) {
271
+ throw new Error(`App '${this.name}': the us-east-1 certificate stack needs a ` +
272
+ "concrete account (got CDK_DEFAULT_ACCOUNT undefined). " +
273
+ "Cross-region references resolve only between env-pinned stacks " +
274
+ "— deploy through the Fjall CLI, or export CDK_DEFAULT_ACCOUNT " +
275
+ "and CDK_DEFAULT_REGION before synthesising.");
276
+ }
277
+ this.stacks[key] = new AwsStack(key, undefined, {
278
+ accountId: account,
279
+ region: "us-east-1",
280
+ crossRegionReferences: true
281
+ });
282
+ }
283
+ return this.stacks[key];
284
+ }
240
285
  /**
241
286
  * Get a VPC by name. If no name is provided, returns the default VPC.
242
287
  *
@@ -437,6 +482,36 @@ export class App extends CdkApp {
437
482
  computeStack.addConstruct(substrate);
438
483
  return substrate;
439
484
  }
485
+ /**
486
+ * Instantiate the self-hosted Buildkite agent fleet on the default compute
487
+ * stack (design `aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`).
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) {
501
+ const computeStack = this.getDefaultComputeStack();
502
+ const configEnvironment = getConfig().environment;
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);
512
+ computeStack.addConstruct(fleet);
513
+ return fleet;
514
+ }
440
515
  /**
441
516
  * Add a database resource to the default database stack using the factory pattern.
442
517
  * Returns the appropriate database type based on the factory used.
@@ -1,26 +1,25 @@
1
1
  import type { Construct } from "constructs";
2
2
  import type { IHostedZone } from "aws-cdk-lib/aws-route53";
3
3
  import type { ICertificate } from "aws-cdk-lib/aws-certificatemanager";
4
- import type { ManualRecord, Route53ApexProps, SubdomainDelegation } from "./interfaces/domain.js";
4
+ import type { ManualRecord, Route53ApexProps } from "./interfaces/domain.js";
5
5
  export interface ApexDomainPatternResult {
6
6
  readonly hostedZone: IHostedZone;
7
7
  readonly certificates: Map<string, ICertificate>;
8
8
  readonly nameServers: string[] | undefined;
9
9
  readonly manualRecords: ManualRecord[];
10
- readonly delegations: SubdomainDelegation[];
11
10
  }
12
11
  /**
13
12
  * Composition for `registrar: "route53"`. Creates (or imports) the apex
14
- * `HostedZone`, wires child-account delegations via `NsRecord` (pointing at
15
- * each child's nameservers published as exports), and composes all user
16
- * records + certificates.
13
+ * `HostedZone` and composes all user records + certificates.
17
14
  *
18
- * Delegation mechanism: the child Fjall account scaffolds its own
19
- * `HostedZone`, and the parent simply writes NS records pointing at the
20
- * child's nameservers (Fn.importValue of the child stack's nameservers
21
- * output). This differs from the legacy `DomainDelegation` pattern, which
22
- * uses `CrossAccountZoneDelegationRecord` to create the child HZ from the
23
- * parent via IAM assume-role. Both patterns are valid for their respective
24
- * semantics see R10 in the Phase 1 plan.
15
+ * Delegation direction (D8): child writes NS. The apex zone's contribution
16
+ * to delegation is the org-gated `DelegationRole` the `HostedZone` wrapper
17
+ * creates on the create path — the delegated child `Domain` assumes that
18
+ * role by LITERAL ARN (`parentDelegationRoleArn`) and UPSERTs its own NS
19
+ * records into this zone via `CrossAccountZoneDelegationRecord`. The old
20
+ * parent-writes `delegations[]` path (NS records from `Fn.importValue` of
21
+ * child nameserver exports) was removed with the H13 legacy layer: it was
22
+ * export-locked, child-first-deploy-ordered, and removal required a parent
23
+ * redeploy.
25
24
  */
26
25
  export declare function composeApexDomain(scope: Construct, props: Route53ApexProps): ApexDomainPatternResult;
@@ -1,24 +1,21 @@
1
- import { Fn } from "aws-cdk-lib";
2
- import { getDomainExportNames } from "@fjall/util";
3
1
  import { HostedZone } from "../../resources/aws/networking/hostedZone.js";
4
2
  import { DomainCertificate } from "../../resources/aws/networking/domainCertificate.js";
5
- import { NsRecord } from "../../resources/aws/networking/dnsRecord/index.js";
6
3
  import { composeTypedDnsRecords } from "./dnsRecordComposer.js";
7
4
  import { toPascalCase, getSafeZoneName } from "../../utils/capitaliseString.js";
8
5
  import { resolveOrgId } from "../../utils/cdkContext.js";
9
6
  /**
10
7
  * Composition for `registrar: "route53"`. Creates (or imports) the apex
11
- * `HostedZone`, wires child-account delegations via `NsRecord` (pointing at
12
- * each child's nameservers published as exports), and composes all user
13
- * records + certificates.
8
+ * `HostedZone` and composes all user records + certificates.
14
9
  *
15
- * Delegation mechanism: the child Fjall account scaffolds its own
16
- * `HostedZone`, and the parent simply writes NS records pointing at the
17
- * child's nameservers (Fn.importValue of the child stack's nameservers
18
- * output). This differs from the legacy `DomainDelegation` pattern, which
19
- * uses `CrossAccountZoneDelegationRecord` to create the child HZ from the
20
- * parent via IAM assume-role. Both patterns are valid for their respective
21
- * semantics see R10 in the Phase 1 plan.
10
+ * Delegation direction (D8): child writes NS. The apex zone's contribution
11
+ * to delegation is the org-gated `DelegationRole` the `HostedZone` wrapper
12
+ * creates on the create path — the delegated child `Domain` assumes that
13
+ * role by LITERAL ARN (`parentDelegationRoleArn`) and UPSERTs its own NS
14
+ * records into this zone via `CrossAccountZoneDelegationRecord`. The old
15
+ * parent-writes `delegations[]` path (NS records from `Fn.importValue` of
16
+ * child nameserver exports) was removed with the H13 legacy layer: it was
17
+ * export-locked, child-first-deploy-ordered, and removal required a parent
18
+ * redeploy.
22
19
  */
23
20
  export function composeApexDomain(scope, props) {
24
21
  const safeZone = toPascalCase(getSafeZoneName(props.zoneName));
@@ -51,22 +48,6 @@ export function composeApexDomain(scope, props) {
51
48
  if (props.records && props.records.length > 0) {
52
49
  composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, props.zoneName, props.records);
53
50
  }
54
- const delegations = props.delegations ?? [];
55
- delegations.forEach((delegation, index) => {
56
- const childZoneName = `${delegation.subdomain}.${props.zoneName}`;
57
- const safeChild = toPascalCase(getSafeZoneName(childZoneName));
58
- const childExports = getDomainExportNames(childZoneName);
59
- // Cross-phase dependency: child stack must deploy first; CFN fails here
60
- // at deploy if the nameservers export does not yet exist.
61
- const nameserversToken = Fn.importValue(childExports.nameservers);
62
- new NsRecord(scope, `${safeZone}Delegation${safeChild}${index}`, {
63
- zone: hostedZoneConstruct.hostedZone,
64
- zoneName: props.zoneName,
65
- recordName: delegation.subdomain,
66
- // Fn.split returns a list token suitable for a multi-value NS record.
67
- values: Fn.split(",", nameserversToken)
68
- });
69
- });
70
51
  return {
71
52
  hostedZone: hostedZoneConstruct.hostedZone,
72
53
  certificates,
@@ -75,8 +56,7 @@ export function composeApexDomain(scope, props) {
75
56
  nameServers: hostedZoneConstruct.isImported
76
57
  ? undefined
77
58
  : (hostedZoneConstruct.nameServers ?? undefined),
78
- manualRecords: [],
79
- delegations
59
+ manualRecords: []
80
60
  };
81
61
  }
82
62
  function normaliseCertificate(cert) {
@@ -0,0 +1,20 @@
1
+ import type { Construct } from "constructs";
2
+ export interface BuildkiteAlarmParams {
3
+ readonly buildkiteOrgSlug: string;
4
+ readonly buildkiteQueue: string;
5
+ readonly autoScalingGroupName: string;
6
+ readonly alarmSnsTopicArn?: string;
7
+ }
8
+ /**
9
+ * The fleet's two Phase-1 alarms (design § D14) — the "~zero babysitting"
10
+ * posture is honest only with these:
11
+ *
12
+ * 1. Scaler heartbeat — `ScheduledJobsCount` goes MISSING for 15 minutes.
13
+ * The scaler publishes every poll, so metric absence means the scaler
14
+ * Lambda is dead or failing; `treatMissingData: BREACHING` is the alarm's
15
+ * entire mechanism.
16
+ * 2. Queued with zero capacity — jobs scheduled while the ASG has no
17
+ * in-service instances for 15 minutes: the fleet cannot boot (AMI gone,
18
+ * quota, subnet failure) while work is waiting.
19
+ */
20
+ export declare function addBuildkiteAlarms(scope: Construct, params: BuildkiteAlarmParams): void;
@@ -0,0 +1,78 @@
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 { Topic } from "aws-cdk-lib/aws-sns";
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 addBuildkiteAlarms(scope, params) {
28
+ const scheduledJobs = new Metric({
29
+ namespace: SCALER_METRICS_NAMESPACE,
30
+ metricName: "ScheduledJobsCount",
31
+ dimensionsMap: {
32
+ Org: params.buildkiteOrgSlug,
33
+ Queue: params.buildkiteQueue
34
+ },
35
+ statistic: "Maximum",
36
+ period: Duration.minutes(5)
37
+ });
38
+ const heartbeatAlarm = new Alarm(scope, "ScalerHeartbeatAlarm", {
39
+ alarmDescription: `Buildkite scaler for queue '${params.buildkiteQueue}' has stopped ` +
40
+ "publishing metrics — scaler Lambda dead or erroring. Jobs will queue " +
41
+ "with no scale-out.",
42
+ metric: scheduledJobs,
43
+ comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD,
44
+ threshold: 0,
45
+ evaluationPeriods: 3,
46
+ treatMissingData: TreatMissingData.BREACHING
47
+ });
48
+ const inServiceInstances = new Metric({
49
+ namespace: "AWS/AutoScaling",
50
+ metricName: "GroupInServiceInstances",
51
+ dimensionsMap: { AutoScalingGroupName: params.autoScalingGroupName },
52
+ statistic: "Maximum",
53
+ period: Duration.minutes(5)
54
+ });
55
+ const queuedWithZeroCapacity = new MathExpression({
56
+ expression: "IF(scheduled > 0 AND inService == 0, 1, 0)",
57
+ usingMetrics: {
58
+ scheduled: scheduledJobs,
59
+ inService: inServiceInstances
60
+ },
61
+ period: Duration.minutes(5)
62
+ });
63
+ const queuedAlarm = new Alarm(scope, "QueuedWithZeroCapacityAlarm", {
64
+ alarmDescription: `Buildkite queue '${params.buildkiteQueue}' has scheduled jobs but ` +
65
+ "zero in-service agents for 15 minutes — the fleet cannot boot " +
66
+ "(AMI, quota, or subnet failure) while work waits.",
67
+ metric: queuedWithZeroCapacity,
68
+ comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
69
+ threshold: 1,
70
+ evaluationPeriods: 3,
71
+ treatMissingData: TreatMissingData.NOT_BREACHING
72
+ });
73
+ if (params.alarmSnsTopicArn !== undefined) {
74
+ const topic = Topic.fromTopicArn(scope, "BuildkiteAlarmTopic", params.alarmSnsTopicArn);
75
+ heartbeatAlarm.addAlarmAction(new SnsAction(topic));
76
+ queuedAlarm.addAlarmAction(new SnsAction(topic));
77
+ }
78
+ }
@@ -0,0 +1,46 @@
1
+ import { type IVpc } from "aws-cdk-lib/aws-ec2";
2
+ import { Construct } from "constructs";
3
+ import { type BuildkitePropsInput } from "./schema.js";
4
+ export type BuildkiteConstructProps = BuildkitePropsInput & {
5
+ readonly vpc: IVpc;
6
+ };
7
+ /**
8
+ * Self-hosted Buildkite agent fleet on the pinned Elastic CI Stack AMIs —
9
+ * scale-to-zero ASG + buildkite-agent-scaler, no secret ever transiting
10
+ * synth, allowlist-only instance IAM. Design:
11
+ * `aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`.
12
+ *
13
+ * A Construct, not a Stack: instantiate via `App.addBuildkite(props)`, which
14
+ * scopes it under the default compute stack so deploy-core's fixed
15
+ * six-category stack selection actually deploys it (the DevSubstrate
16
+ * placement precedent). Network posture is public-subnet + public-IP +
17
+ * zero-ingress (design § D5): `associatePublicIpAddress` MUST be explicit —
18
+ * the wrapper's `!!keyPair` default silently produced no-egress agents in
19
+ * public subnets pre-refactor.
20
+ */
21
+ export declare class Buildkite extends Construct {
22
+ readonly artifactBucketName: string;
23
+ readonly secretsBucketName: string;
24
+ readonly autoScalingGroupName: string;
25
+ constructor(scope: Construct, id: string, props: BuildkiteConstructProps);
26
+ /**
27
+ * Ship the per-job `env` hook into the managed secrets bucket. The
28
+ * s3-secrets-hooks plugin sources this file at the start of EVERY job, so
29
+ * FJALL_API_KEY is read fresh from SSM per job — rotation takes effect on
30
+ * the next job with no instance replacement (design § D4(iii)). The script
31
+ * is configuration, not a secret: shipping it through a CDK asset is fine;
32
+ * the secret VALUE only ever moves SSM → instance at job runtime.
33
+ */
34
+ private addFjallApiKeyEnvHook;
35
+ }
36
+ /**
37
+ * Object key the s3-secrets-hooks plugin sources at the start of every job —
38
+ * upstream contract: `env` at the secrets-bucket root.
39
+ */
40
+ export declare const FJALL_ENV_HOOK_OBJECT_KEY = "env";
41
+ /**
42
+ * The per-job env hook's script content (design § D4(iii)): FJALL_API_KEY is
43
+ * read fresh from SSM per job, so rotation takes effect on the next job with
44
+ * no instance replacement. Pure — the unit test pins the exact content.
45
+ */
46
+ export declare function buildFjallApiKeyEnvHookScript(parameterName: string, region: string): string;