@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
@@ -1,310 +1,12 @@
1
- import { Stack, Tags } from "aws-cdk-lib";
2
- import { CompositePrincipal, ServicePrincipal, PolicyDocument, PolicyStatement } from "aws-cdk-lib/aws-iam";
3
- import { FjallLogger } from "../../utils/validationLogger.js";
4
- import { InstanceClass, InstanceSize, InstanceType, MachineImage, MultipartBody, MultipartUserData, SubnetType, UserData } from "aws-cdk-lib/aws-ec2";
5
- import { safeEbs } from "../../resources/aws/compute/blockDeviceVolume.js";
6
- import { Source } from "aws-cdk-lib/aws-s3-deployment";
7
- import { Vpc } from "../../resources/aws/networking/vpc.js";
8
- import { Policy, Role } from "../../resources/aws/iam/index.js";
9
- import { BucketDeployment, S3Bucket } from "../../resources/aws/storage/index.js";
10
- import { SecureStringParameter } from "../../resources/aws/secrets/index.js";
11
- import { Ec2Instance } from "../../resources/aws/compute/ec2.js";
12
- import { SamApplication } from "../../resources/aws/compute/samApplication.js";
13
- import { applyCostAllocationTags } from "../../utils/costAllocationTags.js";
14
- export var AgentRelease;
15
- (function (AgentRelease) {
16
- AgentRelease["STABLE"] = "stable";
17
- AgentRelease["BETA"] = "beta";
18
- AgentRelease["EDGE"] = "edge";
19
- })(AgentRelease || (AgentRelease = {}));
20
- export const BuildkiteDefaultProps = {
21
- // Buildkite Agent Configuration
22
- elasticStackVersion: "v6.30.0",
23
- keyName: "",
24
- buildkiteAgentRelease: AgentRelease.STABLE,
25
- buildkiteAgentTags: "",
26
- buildkiteAgentTimestampLines: false,
27
- buildkiteAgentExperiments: "",
28
- buildkiteAgentScalerVersion: "1.8.0",
29
- logRetentionDays: 1,
30
- buildkiteAgentTracingBackend: "",
31
- buildkiteAgentCancelGracePeriod: 60,
32
- buildkiteTerminateInstanceAfterJob: false,
33
- buildkiteAdditionalSudoPermissions: "",
34
- buildkiteQueue: "default",
35
- agentsPerInstance: 1,
36
- bootstrapScriptUrl: "",
37
- agentEnvFileUrl: "",
38
- authorizedUsersUrl: "",
39
- // Infrastructure
40
- agentInstanceType: InstanceType.of(InstanceClass.M5, InstanceSize.LARGE),
41
- agentMinInstances: 0,
42
- agentMaxInstances: 10,
43
- agentVolumeSize: 250,
44
- scalerEventSchedulePeriod: "1 minute",
45
- scalerMinPollInterval: "10s",
46
- spotCapacityPercentage: 75,
47
- scaleOutFactor: "1.0",
48
- scaleInIdlePeriod: 600,
49
- scaleOutWaitingForJobs: false,
50
- ecrAccessPolicy: "",
51
- dockerNetworkingProtocol: "ipv4",
52
- enableSecretsPlugin: true,
53
- enableEcrPlugin: true,
54
- enableDockerLoginPlugin: true,
55
- enableDockerUserNamespaceRemap: true,
56
- enableDockerExperimental: false,
57
- enableInstanceStorage: false,
58
- mountTmpfsAtTmp: true,
59
- buildkiteAgentEnableGitMirrors: false,
60
- pipelineSigningKMSKey: "",
61
- pipelineSigningVerificationFailureBehavior: "block",
62
- // Tags
63
- environment: "platform",
64
- owner: "engineer"
65
- };
66
- export class Buildkite extends Stack {
67
- constructor(scope, id, props) {
68
- super(scope, id, props);
69
- applyCostAllocationTags(this, {
70
- service: "buildkite",
71
- domain: "platform",
72
- environment: props.environment || "platform",
73
- owner: props.owner ?? "engineer"
74
- });
75
- const tags = { ...props.tags };
76
- // VPC
77
- const vpc = new Vpc(this, `${id}Vpc`, {
78
- accountId: props.accountId,
79
- subnetConfiguration: [
80
- {
81
- name: `${id}`,
82
- subnetType: SubnetType.PUBLIC
83
- }
84
- ]
85
- });
86
- // Artifacts Bucket
87
- const artifactBucket = new S3Bucket(this, `${id}ArtifactBucket`);
88
- const managedSecretsBucket = new S3Bucket(this, `${id}ManagedSecretsBucket`);
89
- const buildkitePrivateSshKey = process.env.BUILDKITE_PRIVATE_SSH_KEY;
90
- const hasBuildkitePrivateSshKey = buildkitePrivateSshKey !== undefined && buildkitePrivateSshKey !== "";
91
- if (hasBuildkitePrivateSshKey) {
92
- // Synth-time secret bake: Source.data() inlines the env var into the
93
- // CDK asset bundle uploaded to the bootstrap S3 staging bucket. The
94
- // synth host MUST hold the secret in env, and the CDK staging bucket
95
- // inherits the secret's blast radius. Treat as Buildkite-only opt-in.
96
- new BucketDeployment(this, `${id}SshKeyDeployment`, {
97
- sources: [Source.data("private_ssh_key", buildkitePrivateSshKey)],
98
- destinationBucket: managedSecretsBucket
99
- });
100
- }
101
- else {
102
- FjallLogger.warn("'BUILDKITE_PRIVATE_SSH_KEY' environment variable not declared. Skipping upload of private SSH key.");
103
- }
104
- const buildkiteAgentToken = process.env.BUILDKITE_AGENT_TOKEN;
105
- const hasBuildkiteAgentToken = buildkiteAgentToken !== undefined && buildkiteAgentToken !== "";
106
- const agentToken = new SecureStringParameter(this, `${id}AgentToken`, {
107
- name: `/buildkite/agentToken`,
108
- aliasName: `buildkiteAgentToken`,
109
- value: buildkiteAgentToken,
110
- overwrite: hasBuildkiteAgentToken,
111
- tags: tags,
112
- accountId: this.account,
113
- region: this.region
114
- });
115
- // Buildkite Agent Execution Role
116
- const agentRole = new Role(this, `${id}AgentRole`, {
117
- roleName: `${id}AgentRole`,
118
- description: "Buildkite Agent IAM Role",
119
- inlinePolicies: {
120
- ["decryptBuildkiteAgentToken"]: new PolicyDocument({
121
- statements: [
122
- new PolicyStatement({
123
- actions: ["kms:Decrypt"],
124
- resources: [agentToken.cmk.key.keyArn]
125
- })
126
- ]
127
- }),
128
- ["readBuildkiteAgentToken"]: new PolicyDocument({
129
- statements: [
130
- new PolicyStatement({
131
- actions: ["ssm:GetParameter"],
132
- resources: [
133
- `arn:${this.partition}:ssm:${this.region}:${this.account}:parameter${agentToken.name}`
134
- ]
135
- })
136
- ]
137
- }),
138
- ["readManagedSecretsBucket"]: new PolicyDocument({
139
- statements: [
140
- new PolicyStatement({
141
- actions: ["s3:Get*", "s3:List*"],
142
- resources: [
143
- managedSecretsBucket.bucketArn,
144
- `${managedSecretsBucket.bucketArn}/*`
145
- ]
146
- })
147
- ]
148
- }),
149
- ["writeArtifactBucket"]: new PolicyDocument({
150
- statements: [
151
- new PolicyStatement({
152
- actions: [
153
- "s3:GetObject",
154
- "s3:GetObjectAcl",
155
- "s3:GetObjectVersion",
156
- "s3:GetObjectVersionAcl",
157
- "s3:ListBucket",
158
- "s3:PutObject",
159
- "s3:PutObjectAcl",
160
- "s3:PutObjectVersionAcl"
161
- ],
162
- resources: [
163
- artifactBucket.bucketArn,
164
- `${artifactBucket.bucketArn}/*`
165
- ]
166
- })
167
- ]
168
- }),
169
- ["describeInstance"]: new PolicyDocument({
170
- statements: [
171
- new PolicyStatement({
172
- actions: [
173
- "autoscaling:DescribeAutoScalingInstances",
174
- "cloudwatch:PutMetricData",
175
- "cloudformation:DescribeStackResource",
176
- "ec2:DescribeTags"
177
- ],
178
- resources: ["*"]
179
- })
180
- ]
181
- }),
182
- ["logging"]: new PolicyDocument({
183
- statements: [
184
- new PolicyStatement({
185
- actions: [
186
- "logs:CreateLogGroup",
187
- "logs:CreateLogStream",
188
- "logs:PutLogEvents",
189
- "logs:DescribeLogGroups",
190
- "logs:DescribeLogStreams"
191
- ],
192
- resources: ["*"]
193
- })
194
- ]
195
- }),
196
- ["ssm"]: new PolicyDocument({
197
- statements: [
198
- new PolicyStatement({
199
- actions: [
200
- "ssm:DescribeInstanceProperties",
201
- "ssm:ListAssociations",
202
- "ssm:PutInventory",
203
- "ssm:UpdateInstanceInformation",
204
- "ssmmessages:CreateControlChannel",
205
- "ssmmessages:CreateDataChannel",
206
- "ssmmessages:OpenControlChannel",
207
- "ssmmessages:OpenDataChannel",
208
- "ec2messages:AcknowledgeMessage",
209
- "ec2messages:DeleteMessage",
210
- "ec2messages:FailMessage",
211
- "ec2messages:GetEndpoint",
212
- "ec2messages:GetMessages",
213
- "ec2messages:SendReply"
214
- ],
215
- resources: ["*"]
216
- })
217
- ]
218
- })
219
- },
220
- assumedBy: new CompositePrincipal(new ServicePrincipal("autoscaling.amazonaws.com"), new ServicePrincipal("ec2.amazonaws.com"))
221
- });
222
- // Buildkite Agent EC2 User Data
223
- const multipartUserData = new MultipartUserData();
224
- // Cloud Config
225
- const cloudConfigUserData = UserData.forLinux({
226
- shebang: "#cloud-config"
227
- });
228
- cloudConfigUserData.addCommands("cloud_final_modules:", " - [scripts-user, always]");
229
- multipartUserData.addPart(MultipartBody.fromUserData(cloudConfigUserData, "text/cloud-config; charset='us-ascii'"));
230
- // Mount Storage
231
- const mountStorageUserData = UserData.forLinux({
232
- shebang: "#!/bin/bash -v"
233
- });
234
- mountStorageUserData.addCommands(`BUILDKITE_ENABLE_INSTANCE_STORAGE='${props.enableInstanceStorage}' \\`, `BUILDKITE_MOUNT_TMPFS_AT_TMP='${props.mountTmpfsAtTmp}' \\`, "/usr/local/bin/bk-mount-instance-storage.sh");
235
- multipartUserData.addPart(MultipartBody.fromUserData(mountStorageUserData, "text/x-shellscript; charset='us-ascii'"));
236
- // Configure Docker
237
- const configureDockerUserData = UserData.forLinux({
238
- shebang: "#!/bin/bash -v"
239
- });
240
- configureDockerUserData.addCommands(`DOCKER_USERNS_REMAP='${props.enableDockerUserNamespaceRemap}' \\`, `DOCKER_EXPERIMENTAL='${props.enableDockerExperimental}' \\`, `DOCKER_NETWORKING_PROTOCOL='${props.dockerNetworkingProtocol}' \\`, `BUILDKITE_ENABLE_INSTANCE_STORAGE='${props.enableInstanceStorage}' \\`, "/usr/local/bin/bk-configure-docker.sh");
241
- multipartUserData.addPart(MultipartBody.fromUserData(configureDockerUserData, "text/x-shellscript; charset='us-ascii'"));
242
- const installElasticStackUserData = UserData.forLinux({
243
- shebang: "#!/bin/bash -v"
244
- });
245
- installElasticStackUserData.addCommands(`BUILDKITE_STACK_NAME='${id}' \\`, `BUILDKITE_STACK_VERSION='${props.elasticStackVersion}' \\`, `BUILDKITE_SCALE_IN_IDLE_PERIOD='${props.scaleInIdlePeriod}' \\`, `BUILDKITE_SECRETS_BUCKET='${managedSecretsBucket.bucketName}' \\`, `BUILDKITE_SECRETS_BUCKET_REGION='${props.env?.region}' \\`, `BUILDKITE_AGENT_TOKEN_PATH='${agentToken.name}' \\`, `BUILDKITE_AGENTS_PER_INSTANCE='${props.agentsPerInstance}' \\`, `BUILDKITE_AGENT_TAGS='${props.buildkiteAgentTags}' \\`, `BUILDKITE_AGENT_TIMESTAMP_LINES='${props.buildkiteAgentTimestampLines}' \\`, `BUILDKITE_AGENT_EXPERIMENTS='${props.buildkiteAgentExperiments}' \\`, `BUILDKITE_AGENT_TRACING_BACKEND='${props.buildkiteAgentTracingBackend}' \\`, `BUILDKITE_AGENT_RELEASE='${props.buildkiteAgentRelease}' \\`, `BUILDKITE_AGENT_CANCEL_GRACE_PERIOD='${props.buildkiteAgentCancelGracePeriod}' \\`, `BUILDKITE_AGENT_SIGNING_KMS_KEY='${props.pipelineSigningKMSKey}' \\`, `BUILDKITE_AGENT_SIGNING_FAILURE_BEHAVIOR='${props.pipelineSigningVerificationFailureBehavior}' \\`, `BUILDKITE_QUEUE='${props.buildkiteQueue}' \\`, `BUILDKITE_AGENT_ENABLE_GIT_MIRRORS='${props.buildkiteAgentEnableGitMirrors}' \\`, `BUILDKITE_ELASTIC_BOOTSTRAP_SCRIPT='${props.bootstrapScriptUrl}' \\`, `BUILDKITE_ENV_FILE_URL='${props.agentEnvFileUrl}' \\`, `BUILDKITE_ENABLE_INSTANCE_STORAGE='${props.enableInstanceStorage}' \\`, `BUILDKITE_AUTHORIZED_USERS_URL='${props.authorizedUsersUrl}' \\`, `BUILDKITE_ECR_POLICY='${props.ecrAccessPolicy}' \\`, `BUILDKITE_TERMINATE_INSTANCE_AFTER_JOB='${props.buildkiteTerminateInstanceAfterJob}' \\`, `BUILDKITE_ADDITIONAL_SUDO_PERMISSIONS='${props.buildkiteAdditionalSudoPermissions}' \\`, `AWS_DEFAULT_REGION='${props.env?.region}' \\`, `SECRETS_PLUGIN_ENABLED='${props.enableSecretsPlugin}' \\`, `ECR_PLUGIN_ENABLED='${props.enableEcrPlugin}' \\`, `DOCKER_LOGIN_PLUGIN_ENABLED='${props.enableDockerLoginPlugin}' \\`, `DOCKER_EXPERIMENTAL='${props.enableDockerExperimental}' \\`, `DOCKER_USERNS_REMAP='${props.enableDockerUserNamespaceRemap}' \\`, `AWS_REGION='${props.env?.region}' \\`, "/usr/local/bin/bk-install-elastic-stack.sh");
246
- // Merge User Data
247
- multipartUserData.addPart(MultipartBody.fromUserData(installElasticStackUserData, "text/x-shellscript; charset='us-ascii'"));
248
- const ec2Instance = new Ec2Instance(this, `${id}Agent`, {
249
- serviceName: `${id}Agent`,
250
- vpc,
251
- vpcSubnets: { subnetType: SubnetType.PUBLIC },
252
- instanceType: props.agentInstanceType.toString(),
253
- machineImage: MachineImage.lookup({
254
- name: "buildkite-stack-linux-x86_64-*",
255
- owners: ["172840064832"],
256
- filters: {
257
- ["block-device-mapping.volume-type"]: ["gp3"]
258
- }
259
- }),
260
- userData: multipartUserData,
261
- role: agentRole,
262
- blockDevices: [
263
- {
264
- deviceName: "/dev/xvda",
265
- volume: safeEbs(props.agentVolumeSize)
266
- }
267
- ],
268
- minCapacity: props.agentMinInstances,
269
- maxCapacity: props.agentMaxInstances,
270
- spotCapacityPercentage: props.spotCapacityPercentage
271
- });
272
- Tags.of(ec2Instance).add("Role", "buildkiteAgent");
273
- Tags.of(ec2Instance).add("BuildkiteAgentRelease", props.buildkiteAgentRelease);
274
- Tags.of(ec2Instance).add("BuildkiteQueue", props.buildkiteQueue);
275
- const autoScalingGroup = ec2Instance.getAutoScalingGroup();
276
- agentRole.attachInlinePolicy(new Policy(this, `${id}UpdateAutoScalingGroupPolicy`, {
277
- statements: [
278
- new PolicyStatement({
279
- actions: [
280
- "autoscaling:SetInstanceHealth",
281
- "autoscaling:TerminateInstanceInAutoScalingGroup"
282
- ],
283
- resources: [`${autoScalingGroup.autoScalingGroupArn}`]
284
- })
285
- ]
286
- }));
287
- new SamApplication(this, `${id}AgentScaler`, {
288
- applicationId: "arn:aws:serverlessrepo:us-east-1:172840064832:applications/buildkite-agent-scaler",
289
- semanticVersion: props.buildkiteAgentScalerVersion,
290
- parameters: {
291
- BuildkiteAgentTokenParameter: `${agentToken.name}`,
292
- BuildkiteAgentTokenParameterStoreKMSKey: agentToken.cmk.key.keyId,
293
- RolePermissionsBoundaryARN: "",
294
- BuildkiteQueue: props.buildkiteQueue,
295
- AgentsPerInstance: `${props.agentsPerInstance}`,
296
- MinSize: `${props.agentMinInstances}`,
297
- MaxSize: `${props.agentMaxInstances}`,
298
- AgentAutoScaleGroup: autoScalingGroup.autoScalingGroupName,
299
- ScaleOutFactor: props.scaleOutFactor,
300
- ScaleOutForWaitingJobs: `${props.scaleOutWaitingForJobs}`,
301
- EventSchedulePeriod: props.scalerEventSchedulePeriod,
302
- MinPollInterval: props.scalerMinPollInterval,
303
- LogRetentionDays: `${props.logRetentionDays}`
304
- },
305
- costAllocationService: "buildkite",
306
- costAllocationDomain: "buildkite-agent-scaler"
307
- });
308
- }
309
- }
310
- export default Buildkite;
1
+ /**
2
+ * Self-hosted Buildkite agent fleet barrel for the `buildkite/` modules.
3
+ * The pre-2026-07 `Buildkite extends Stack` implementation is replaced by a
4
+ * Construct instantiated via `App.addBuildkite()`; design:
5
+ * `aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`.
6
+ */
7
+ export { Buildkite, FJALL_ENV_HOOK_OBJECT_KEY, buildFjallApiKeyEnvHookScript } from "./buildkite/buildkite.js";
8
+ export { BUILDKITE_AMI_OWNER_ACCOUNT_ID, BUILDKITE_CPU_ARCHITECTURES, BUILDKITE_STACK_PINS, resolvePinnedAmiId, resolveScalerSarApplicationArn } from "./buildkite/pins.js";
9
+ export { BuildkitePropsSchema, validateBuildkiteProps } from "./buildkite/schema.js";
10
+ export { BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST, buildAgentRole } from "./buildkite/iam.js";
11
+ 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";
@@ -1,23 +1,38 @@
1
- import { Fn } from "aws-cdk-lib";
1
+ import { Fn, Token } from "aws-cdk-lib";
2
2
  import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
3
3
  import { CloudFrontDistribution } from "../../resources/aws/cdn/index.js";
4
4
  import { isStorage } from "./storage.js";
5
5
  import { isCompute, isEcsCompute, isLambdaCompute } from "./compute.js";
6
- import { FjallLogger } from "../../utils/validationLogger.js";
7
6
  /**
8
- * Validates CDN props and logs warnings for misconfigured options.
7
+ * Validates CDN props synth-time hard errors for shapes CloudFormation
8
+ * would only reject at deploy.
9
9
  */
10
10
  function validateCdnProps(props) {
11
11
  const hasCertificate = !!(props.certificate || props.certificateArn);
12
- // Validate certificate region warning
13
- if (hasCertificate && props.domainNames && props.domainNames.length > 0) {
14
- FjallLogger.info("CloudFront certificates must be in us-east-1 region. " +
15
- "Ensure your certificate was created in us-east-1.");
12
+ // CloudFront accepts viewer certificates from us-east-1 only. A literal
13
+ // ARN is inspectable at synth (design H3/D3 the old advisory log is
14
+ // replaced by real us-east-1 provisioning in the patterns; this assert
15
+ // covers the BYO-ARN escape hatch), so fail here with the cure rather
16
+ // than at CloudFormation with InvalidViewerCertificate.
17
+ if (props.certificateArn !== undefined &&
18
+ !Token.isUnresolved(props.certificateArn)) {
19
+ const certificateRegion = props.certificateArn.split(":")[3];
20
+ if (certificateRegion !== "us-east-1") {
21
+ throw new Error(`CDN 'certificateArn' is in region '${certificateRegion}' ` +
22
+ `(got '${props.certificateArn}'). CloudFront accepts viewer ` +
23
+ "certificates from us-east-1 only — pass the ARN of a us-east-1 " +
24
+ "certificate, or set the pattern's domain zone identity so it " +
25
+ "provisions one.");
26
+ }
16
27
  }
17
- // Validate domain names require certificate
28
+ // Aliases without a viewer certificate fail at CloudFormation deploy; fail
29
+ // at synth with the cure instead.
18
30
  if (props.domainNames && props.domainNames.length > 0 && !hasCertificate) {
19
- FjallLogger.warn("'domainNames' provided but no 'certificate' specified. " +
20
- "Custom domain names require an ACM certificate in us-east-1.");
31
+ throw new Error(`CDN 'domainNames' [${props.domainNames.join(", ")}] were provided ` +
32
+ "without a certificate. CloudFront serves custom domain names only " +
33
+ "with a us-east-1 ACM certificate — pass 'certificate' or " +
34
+ "'certificateArn', or configure the pattern's 'domain' with a zone " +
35
+ "identity so it provisions one.");
21
36
  }
22
37
  // Validate ALB origin
23
38
  if (props.originType === "alb") {
@@ -25,7 +25,7 @@ import { FjallLogger } from "../../utils/validationLogger.js";
25
25
  import { VALIDATION_PATTERNS } from "@fjall/generator";
26
26
  import { evaluateBakeGuard } from "@fjall/util/docker";
27
27
  import { toKebab, buildParameterPath } from "@fjall/util";
28
- import { validateSecretName } from "../../resources/aws/compute/ecsValidation.js";
28
+ import { validateEcsDomainConfig, validateSecretName } from "../../resources/aws/compute/ecsValidation.js";
29
29
  import { COMPUTE_DEFAULTS, collectImportedSecretNames } from "./compute.js";
30
30
  import { isHookMigrations } from "./computeEcsTypes.js";
31
31
  export { ScalingType } from "./computeEcsTypes.js";
@@ -81,6 +81,11 @@ function validateSeparateTaskDef(serviceName, separateTaskDef) {
81
81
  * Extracted for clarity and detail parity with database/network patterns.
82
82
  */
83
83
  export function validateEcsProps(props) {
84
+ // Mirror of the resources-layer domainConfig checks (H8 legacy `region`
85
+ // rejection, routingPolicy shape) — defence-in-depth per generator-standards
86
+ // § "Validate at the Lowest Layer the Field Belongs To". The cluster name is
87
+ // the construct id, unavailable here, so errors prefix with the domain name.
88
+ validateEcsDomainConfig(props.cluster?.domainConfig);
84
89
  // Validate services array exists and is not empty
85
90
  if (!props.services || props.services.length === 0) {
86
91
  throw new Error("At least one service must be specified in 'services' array.");
@@ -522,6 +522,10 @@ export interface EcsClusterConfig {
522
522
  * Domain for HTTPS access.
523
523
  * - Omit: ALB created with default DNS (*.elb.amazonaws.com)
524
524
  * - Specified: Creates ACM certificate + Route53 DNS A record
525
+ *
526
+ * Requires `domainConfig` to name a zone source (`managedDomain` or
527
+ * `hostedZone`) — Fjall never creates a hosted zone inside an app stack
528
+ * (design D1/H1), so a bare `domain` with no zone source throws at synth.
525
529
  */
526
530
  domain?: string;
527
531
  /**
@@ -538,9 +542,10 @@ export interface EcsClusterConfig {
538
542
  */
539
543
  directAccess?: boolean;
540
544
  /**
541
- * Advanced domain configuration for routing policies (latency, weighted, geo).
542
- * Only used when domain is specified.
543
- * Allows for multi-region deployments with advanced DNS routing.
545
+ * Domain configuration: zone source (managed domain or imported zone),
546
+ * certificate, and routing policy (latency via `routingPolicy`, weighted,
547
+ * geo). Required for any custom domain; enables multi-region deployments
548
+ * with advanced DNS routing.
544
549
  */
545
550
  domainConfig?: DomainConfig;
546
551
  /**
@@ -9,9 +9,18 @@ export interface DelegatedDomainPatternResult {
9
9
  readonly manualRecords: ManualRecord[];
10
10
  }
11
11
  /**
12
- * Composition for `registrar: "external-delegated"`. Creates a hosted zone
13
- * for `{delegatedSubdomain}.{zoneName}` — the user adds NS records at their
14
- * external registrar to delegate the subdomain to Fjall. No cross-account
15
- * delegation role is required because Fjall is not creating the parent zone.
12
+ * Composition for `registrar: "external-delegated"` the delegated-child
13
+ * topology. Creates a hosted zone for `{delegatedSubdomain}.{zoneName}` and,
14
+ * when `parentDelegationRoleArn` is present, UPSERTs the child NS into the
15
+ * parent zone via `CrossAccountZoneDelegationRecord` (D8 child-writes
16
+ * delegation; same shape as devSubstrate Case 1). The role ARN MUST be a
17
+ * literal — validated in `validateDomainProps` (VD2-a). When absent, the
18
+ * user delegates out of band (external registrar / manual NS) and a synth
19
+ * warning flags that the zone will not resolve until they do.
20
+ *
21
+ * `phase: "zone"` early-returns before certificate issuance (R2 cert-hang
22
+ * guard, shared `DomainDeployPhase` contract with `DevSubstrate`): issuing a
23
+ * DNS-validated certificate before the delegation has propagated hangs ACM
24
+ * until CloudFormation rolls back (~1 h).
16
25
  */
17
26
  export declare function composeDelegatedDomain(scope: Construct, props: ExternalDelegatedProps): DelegatedDomainPatternResult;
@@ -1,22 +1,75 @@
1
+ import { Annotations } from "aws-cdk-lib";
2
+ import { Role } from "../../resources/aws/iam/index.js";
1
3
  import { HostedZone } from "../../resources/aws/networking/hostedZone.js";
4
+ import { CrossAccountDelegationRecord } from "../../resources/aws/networking/crossAccountDelegationRecord.js";
2
5
  import { DomainCertificate } from "../../resources/aws/networking/domainCertificate.js";
3
6
  import { composeTypedDnsRecords } from "./dnsRecordComposer.js";
4
7
  import { toPascalCase, getSafeZoneName } from "../../utils/capitaliseString.js";
8
+ import { DOMAIN_DEPLOY_DEFAULT_PHASE } from "../../utils/domainTypes.js";
5
9
  /**
6
- * Composition for `registrar: "external-delegated"`. Creates a hosted zone
7
- * for `{delegatedSubdomain}.{zoneName}` — the user adds NS records at their
8
- * external registrar to delegate the subdomain to Fjall. No cross-account
9
- * delegation role is required because Fjall is not creating the parent zone.
10
+ * Composition for `registrar: "external-delegated"` the delegated-child
11
+ * topology. Creates a hosted zone for `{delegatedSubdomain}.{zoneName}` and,
12
+ * when `parentDelegationRoleArn` is present, UPSERTs the child NS into the
13
+ * parent zone via `CrossAccountZoneDelegationRecord` (D8 child-writes
14
+ * delegation; same shape as devSubstrate Case 1). The role ARN MUST be a
15
+ * literal — validated in `validateDomainProps` (VD2-a). When absent, the
16
+ * user delegates out of band (external registrar / manual NS) and a synth
17
+ * warning flags that the zone will not resolve until they do.
18
+ *
19
+ * `phase: "zone"` early-returns before certificate issuance (R2 cert-hang
20
+ * guard, shared `DomainDeployPhase` contract with `DevSubstrate`): issuing a
21
+ * DNS-validated certificate before the delegation has propagated hangs ACM
22
+ * until CloudFormation rolls back (~1 h).
10
23
  */
11
24
  export function composeDelegatedDomain(scope, props) {
12
25
  const effectiveZone = `${props.delegatedSubdomain}.${props.zoneName}`;
13
26
  const safeZone = toPascalCase(getSafeZoneName(effectiveZone));
14
27
  const hostedZoneConstruct = new HostedZone(scope, `${safeZone}HostedZone`, {
15
28
  zoneName: effectiveZone,
29
+ // The PARENT account owns the delegation role — this is the child.
16
30
  createDelegationRole: false,
17
31
  costAllocationEnvironment: props.costAllocationEnvironment,
18
32
  costAllocationDomain: props.zoneName
19
33
  });
34
+ // D8 child-writes delegation. The in-stack custom resource assumes the
35
+ // parent account's DelegationRole by LITERAL ARN (Role.fromRoleArn) — NOT
36
+ // Fn.importValue, which resolves same-account only (VD2-a). The parent
37
+ // DelegationRole trusts the whole org (OrganizationPrincipal), so a child
38
+ // account can assume it. The CLI injects the ARN via
39
+ // ManagedDomainBinding.delegationRoleArn at deploy time.
40
+ if (props.parentDelegationRoleArn !== undefined) {
41
+ const parentRole = Role.fromRoleArn(scope, `${safeZone}ParentDelegationRole`, props.parentDelegationRoleArn);
42
+ new CrossAccountDelegationRecord(scope, `${safeZone}DelegationRecord`, {
43
+ delegationRole: parentRole,
44
+ delegatedZone: hostedZoneConstruct.hostedZone,
45
+ delegatedZoneName: effectiveZone,
46
+ parentHostedZoneName: props.zoneName,
47
+ costAllocationEnvironment: props.costAllocationEnvironment,
48
+ costAllocationDomain: props.zoneName
49
+ });
50
+ }
51
+ else {
52
+ Annotations.of(scope).addWarning(`Zone '${effectiveZone}' will not resolve until it is delegated: no ` +
53
+ `parentDelegationRoleArn was provided, so no NS records are written ` +
54
+ `into '${props.zoneName}'. Deploy via the Fjall CLI (which injects ` +
55
+ `the parent DelegationRole ARN from the managed-domain binding) or ` +
56
+ `add the zone's NS records at the parent registrar manually.`);
57
+ }
58
+ const nameServers = hostedZoneConstruct.nameServers ?? [];
59
+ const records = props.records ?? [];
60
+ if (records.length > 0) {
61
+ composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, effectiveZone, records);
62
+ }
63
+ // Step 2 of the R2 two-step gate: issuing the cert before the delegation
64
+ // NS has propagated hangs ACM DNS-validation, so "zone" stops here.
65
+ if ((props.phase ?? DOMAIN_DEPLOY_DEFAULT_PHASE) !== "full") {
66
+ return {
67
+ hostedZone: hostedZoneConstruct.hostedZone,
68
+ certificates: new Map(),
69
+ nameServers,
70
+ manualRecords: []
71
+ };
72
+ }
20
73
  const certificates = new Map();
21
74
  (props.certificates ?? []).forEach((cert, index) => {
22
75
  const normalised = normaliseCertificate(cert);
@@ -31,10 +84,6 @@ export function composeDelegatedDomain(scope, props) {
31
84
  });
32
85
  certificates.set(normalised.domainName, dc.certificate);
33
86
  });
34
- if (props.records && props.records.length > 0) {
35
- composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, effectiveZone, props.records);
36
- }
37
- const nameServers = hostedZoneConstruct.nameServers ?? [];
38
87
  return {
39
88
  hostedZone: hostedZoneConstruct.hostedZone,
40
89
  certificates,
@@ -1,5 +1,6 @@
1
1
  import { Construct } from "constructs";
2
2
  import type App from "../../app.js";
3
+ import { type DomainDeployPhase } from "../../utils/domainTypes.js";
3
4
  /**
4
5
  * Props for the internal `DevSubstrate` construct.
5
6
  *
@@ -44,7 +45,7 @@ export interface IDevSubstrateProps {
44
45
  * only after the delegated NS has propagated, else ACM DNS-validation hangs.
45
46
  * Defaults to `"full"`. Inert unless `domain` is set.
46
47
  */
47
- readonly phase?: "zone" | "full";
48
+ readonly phase?: DomainDeployPhase;
48
49
  /**
49
50
  * Public domain the substrate hangs a `dev.<appDomain>` child zone under. When
50
51
  * omitted, the substrate stands up no zone/cert (early milestone — G2 reaches
@@ -24,6 +24,7 @@ import { CrossAccountDelegationRecord } from "../../resources/aws/networking/cro
24
24
  import { DomainCertificate } from "../../resources/aws/networking/domainCertificate.js";
25
25
  import { Role } from "../../resources/aws/iam/role.js";
26
26
  import { toPascalCase } from "../../utils/capitaliseString.js";
27
+ import { DOMAIN_DEPLOY_DEFAULT_PHASE } from "../../utils/domainTypes.js";
27
28
  import { DEV_AURORA_DEFAULTS } from "../../resources/aws/database/rdsDefaults.js";
28
29
  import { DEV_APP_TAG_KEY, DEV_TAG_KEY, DEV_TAG_VALUE } from "../../utils/devSubstrateTags.js";
29
30
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -54,12 +55,11 @@ const DEV_SUBSTRATE_PARENT_ROLE_ID = "DevSubstrateParentDelegationRole";
54
55
  /** Construct ids for the substrate's wildcard cert + :443 HTTPS listener (A6). */
55
56
  const DEV_SUBSTRATE_CERT_ID = "DevSubstrateCertificate";
56
57
  const DEV_SUBSTRATE_HTTPS_LISTENER_ID = "DevSubstrateHttpsListener";
57
- /**
58
- * Default deploy phase when `phase` is omitted — the cert + :443 listener build.
59
- * The two-step guard (design R2) sets `"zone"` explicitly for step 1, so the
60
- * safe default for a single-shot deploy is the complete substrate.
61
- */
62
- const DEV_SUBSTRATE_DEFAULT_PHASE = "full";
58
+ // Default deploy phase when `phase` is omitted — the cert + :443 listener
59
+ // build. Shared with the delegated `Domain` topology via
60
+ // `DOMAIN_DEPLOY_DEFAULT_PHASE` (utils/domainTypes.ts): the R2 two-step guard
61
+ // sets `"zone"` explicitly for step 1, so the safe default for a single-shot
62
+ // deploy is the complete substrate.
63
63
  /**
64
64
  * The substrate's database mode, surfaced to slot-ops as an export so the
65
65
  * hand-created G2 slot knows whether to wire Aurora Data-API creds or a
@@ -142,6 +142,11 @@ export class DevSubstrate extends Construct {
142
142
  // exposes for the cluster ARN; it returns databaseCluster.clusterArn.
143
143
  dbClusterArn: database.getSnapshotTarget().arn,
144
144
  dbSecretArn: database.getCredentials().secret.secretArn,
145
+ // Proxy-less substrate ⇒ getHostEndpoint() is the cluster writer endpoint;
146
+ // it + the port ride the exports because the dev role cannot rds:Describe*
147
+ // at runtime to resolve them.
148
+ dbWriterEndpoint: database.getHostEndpoint(),
149
+ dbPort: database.getHostPort(),
145
150
  slotExecutionRoleArn: slotExecutionRole.roleArn,
146
151
  slotRepositoryUri: slotRepository.repositoryUri,
147
152
  wakerTargetGroupArn: wakerTargetGroup.targetGroupArn,
@@ -244,7 +249,10 @@ export class DevSubstrate extends Construct {
244
249
  // fence-pinned name — the reaper finds it by the subtree fence tags. A pinned
245
250
  // name would only reintroduce the 32-char ALB-name truncation dance.
246
251
  const loadBalancer = createApplicationLoadBalancer(this, DEV_SUBSTRATE_ALB_ID, { vpc, internetFacing: true, securityGroup: albSecurityGroup });
247
- slotSecurityGroup.connections.allowFrom(albSecurityGroup, Port.tcp(DEV_SUBSTRATE_SLOT_PORT), "Dev substrate ALB → slot tasks");
252
+ slotSecurityGroup.connections.allowFrom(albSecurityGroup, Port.tcp(DEV_SUBSTRATE_SLOT_PORT),
253
+ // EC2 SG-rule descriptions reject non-ASCII (allowed set is
254
+ // `a-zA-Z0-9. _-:/()#,@[]+=&;{}!$*` — no arrow, em-dash, or even `>`).
255
+ "Dev substrate ALB to slot tasks");
248
256
  // Unconditional 404 default — the substrate's only routes are per-slot
249
257
  // host-header rules, so an unmatched host must 404, never fall through (the ECS
250
258
  // path's 404 is conditional on >=2 routes). The :443 listener + wildcard cert
@@ -327,7 +335,7 @@ export class DevSubstrate extends Construct {
327
335
  }
328
336
  // Step 2 of the R2 two-step gate: synthesising the cert before the step-1 NS
329
337
  // has propagated hangs ACM DNS-validation for hours, so "zone" stops here.
330
- if ((props.phase ?? DEV_SUBSTRATE_DEFAULT_PHASE) !== "full")
338
+ if ((props.phase ?? DOMAIN_DEPLOY_DEFAULT_PHASE) !== "full")
331
339
  return { devDomain: childZoneName };
332
340
  // exportCertificateArn:false — consumed in-stack by the :443 listener (slots
333
341
  // reference the exported LISTENER arn, not the cert), and the wildcard `*.`
@@ -375,6 +383,8 @@ export class DevSubstrate extends Construct {
375
383
  emit(this, "AlbCloudWatchName", values.albCloudWatchName);
376
384
  emit(databaseScope, "DbClusterArn", values.dbClusterArn);
377
385
  emit(databaseScope, "DbSecretArn", values.dbSecretArn);
386
+ emit(databaseScope, "DbWriterEndpoint", values.dbWriterEndpoint);
387
+ emit(databaseScope, "DbPort", values.dbPort);
378
388
  emit(this, "SlotExecutionRoleArn", values.slotExecutionRoleArn);
379
389
  emit(this, "SlotRepositoryUri", values.slotRepositoryUri);
380
390
  emit(this, "WakerTargetGroupArn", values.wakerTargetGroupArn);
@@ -1,15 +1,8 @@
1
1
  import type { Construct } from "constructs";
2
2
  import type { IHostedZone } from "aws-cdk-lib/aws-route53";
3
- import type { DnsRecord, DnsRecordInput } from "./interfaces/domain.js";
3
+ import type { DnsRecord } from "./interfaces/domain.js";
4
4
  /**
5
- * Legacy composer consumed by `DomainFactory` (deleted in Phase 2). Accepts
6
- * the `DnsRecordInput` shape from the BIND-primary era where ALIAS sentinels
7
- * live in `value`. Typed targets are not supported here; use
8
- * `composeTypedDnsRecords` instead.
9
- */
10
- export declare function composeDnsRecords(scope: Construct, zone: IHostedZone, zoneName: string, records: DnsRecordInput[]): void;
11
- /**
12
- * Typed composer consumed by the new `Domain` construct. Accepts the
5
+ * Typed composer consumed by the `Domain` construct. Accepts the
13
6
  * discriminated `DnsRecord` union (`StandardRecord | AliasRecord`).
14
7
  *
15
8
  * `AliasRecord` entries MUST carry a `target` produced by `fjallApp()`,