@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
@@ -0,0 +1,233 @@
1
+ import { CfnOutput, Duration, Stack, Token } from "aws-cdk-lib";
2
+ import { InstanceArchitecture, InstanceType, MachineImage, SubnetType } from "aws-cdk-lib/aws-ec2";
3
+ import { PolicyStatement } from "aws-cdk-lib/aws-iam";
4
+ import { Source } from "aws-cdk-lib/aws-s3-deployment";
5
+ import { Construct } from "constructs";
6
+ import { safeEbs } from "../../../resources/aws/compute/blockDeviceVolume.js";
7
+ import { Ec2Instance } from "../../../resources/aws/compute/ec2.js";
8
+ import { SamApplication } from "../../../resources/aws/compute/samApplication.js";
9
+ import { Policy } from "../../../resources/aws/iam/index.js";
10
+ import { BucketDeployment, S3Bucket } from "../../../resources/aws/storage/index.js";
11
+ import { applyCostAllocationTags } from "../../../utils/costAllocationTags.js";
12
+ import { addBuildkiteAlarms } from "./alarms.js";
13
+ import { buildAgentRole } from "./iam.js";
14
+ import { BUILDKITE_STACK_PINS, resolvePinnedAmiId, resolveScalerSarApplicationArn } from "./pins.js";
15
+ import { validateBuildkiteProps } from "./schema.js";
16
+ import { buildBuildkiteUserData } from "./userData.js";
17
+ /**
18
+ * Self-hosted Buildkite agent fleet on the pinned Elastic CI Stack AMIs —
19
+ * scale-to-zero ASG + buildkite-agent-scaler, no secret ever transiting
20
+ * synth, allowlist-only instance IAM. Design:
21
+ * `aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`.
22
+ *
23
+ * A Construct, not a Stack: instantiate via `App.addBuildkite(props)`, which
24
+ * scopes it under the default compute stack so deploy-core's fixed
25
+ * six-category stack selection actually deploys it (the DevSubstrate
26
+ * placement precedent). Network posture is public-subnet + public-IP +
27
+ * zero-ingress (design § D5): `associatePublicIpAddress` MUST be explicit —
28
+ * the wrapper's `!!keyPair` default silently produced no-egress agents in
29
+ * public subnets pre-refactor.
30
+ */
31
+ export class Buildkite extends Construct {
32
+ artifactBucketName;
33
+ secretsBucketName;
34
+ autoScalingGroupName;
35
+ constructor(scope, id, props) {
36
+ super(scope, id);
37
+ const { vpc, ...plainProps } = props;
38
+ const config = validateBuildkiteProps(plainProps);
39
+ const stack = Stack.of(this);
40
+ if (Token.isUnresolved(stack.region)) {
41
+ throw new Error("Buildkite requires a concrete env region at synth (the pinned AMI " +
42
+ "is region-specific). Pass env: { account, region } to the App.");
43
+ }
44
+ const region = stack.region;
45
+ const architecture = deriveCpuArchitecture(config.instanceType);
46
+ const amiId = resolvePinnedAmiId(region, architecture);
47
+ applyCostAllocationTags(this, {
48
+ service: "buildkite",
49
+ domain: "platform",
50
+ ...(config.costAllocationEnvironment !== undefined && {
51
+ environment: config.costAllocationEnvironment
52
+ }),
53
+ ...(config.costAllocationOwner !== undefined && {
54
+ owner: config.costAllocationOwner
55
+ })
56
+ });
57
+ const artifactBucket = new S3Bucket(this, `${id}ArtifactBucket`);
58
+ const managedSecretsBucket = new S3Bucket(this, `${id}ManagedSecretsBucket`);
59
+ if (config.fjallApiKeySsmParameterName !== undefined) {
60
+ this.addFjallApiKeyEnvHook(id, managedSecretsBucket, config.fjallApiKeySsmParameterName, region);
61
+ }
62
+ const parameterArn = (name) => `arn:${stack.partition}:ssm:${region}:${stack.account}:parameter${name}`;
63
+ const agentRole = buildAgentRole(this, `${id}AgentRole`, {
64
+ agentTokenParameterArn: parameterArn(config.agentTokenSsmParameterName),
65
+ ...(config.agentTokenKmsKeyArn !== undefined && {
66
+ agentTokenKmsKeyArn: config.agentTokenKmsKeyArn
67
+ }),
68
+ ...(config.fjallApiKeySsmParameterName !== undefined && {
69
+ fjallApiKeyParameterArn: parameterArn(config.fjallApiKeySsmParameterName)
70
+ }),
71
+ secretsBucketArn: managedSecretsBucket.bucketArn,
72
+ artifactBucketArn: artifactBucket.bucketArn,
73
+ stackArn: stack.stackId,
74
+ logGroupArnPattern: `arn:${stack.partition}:logs:${region}:${stack.account}:log-group:/buildkite/*`
75
+ });
76
+ const userData = buildBuildkiteUserData(config, {
77
+ stackName: stack.stackName,
78
+ region,
79
+ secretsBucketName: managedSecretsBucket.bucketName,
80
+ artifactBucketName: artifactBucket.bucketName
81
+ });
82
+ // Deliberate deviation from upstream's InstanceScaleInProtection
83
+ // (design § D7): the wrapper's `newInstancesProtectedFromScaleIn: false`
84
+ // invariant wins (scale-in-protected instances wedge CFN rollback). Safe
85
+ // because the scaler runs with `DisableScaleIn: "true"` (pinned below) so
86
+ // it never reduces DesiredCapacity — instances leave only by
87
+ // self-termination (scale-in idle / disconnect-after-uptime) or the ASG
88
+ // `maxInstanceLifetime` replacement.
89
+ //
90
+ // `ssmSessionPermissions: false`: the wrapper default attaches
91
+ // `AmazonSSMManagedInstanceCore`, whose account-wide `ssm:GetParameter`
92
+ // would let any build job read every SSM parameter in the account —
93
+ // breaking § D9's allowlist-only posture. The agent role's scoped
94
+ // `sessionManager` inline policy carries the Session Manager actions.
95
+ const ec2Instance = new Ec2Instance(this, `${id}Agent`, {
96
+ serviceName: `${id}Agent`,
97
+ vpc,
98
+ vpcSubnets: { subnetType: SubnetType.PUBLIC },
99
+ associatePublicIpAddress: true,
100
+ instanceType: config.instanceType,
101
+ machineImage: MachineImage.genericLinux({ [region]: amiId }),
102
+ userData,
103
+ role: agentRole,
104
+ ssmSessionPermissions: false,
105
+ blockDevices: [
106
+ {
107
+ deviceName: "/dev/xvda",
108
+ volume: safeEbs(config.agentVolumeSizeGib)
109
+ }
110
+ ],
111
+ minCapacity: config.agentMinInstances,
112
+ maxCapacity: config.agentMaxInstances,
113
+ spotCapacityPercentage: config.spotCapacityPercentage,
114
+ maxInstanceLifetime: Duration.days(config.maxInstanceLifetimeDays),
115
+ tags: {
116
+ Role: "buildkite-agent",
117
+ BuildkiteQueue: config.buildkiteQueue,
118
+ BuildkiteAgentRelease: config.buildkiteAgentRelease,
119
+ AgentsPerInstance: `${config.agentsPerInstance}`
120
+ }
121
+ });
122
+ const autoScalingGroup = ec2Instance.getAutoScalingGroup();
123
+ agentRole.attachInlinePolicy(new Policy(this, `${id}AgentScaleInPolicy`, {
124
+ statements: [
125
+ new PolicyStatement({
126
+ actions: [
127
+ "autoscaling:SetInstanceHealth",
128
+ "autoscaling:TerminateInstanceInAutoScalingGroup"
129
+ ],
130
+ resources: [autoScalingGroup.autoScalingGroupArn]
131
+ })
132
+ ]
133
+ }));
134
+ new SamApplication(this, `${id}AgentScaler`, {
135
+ applicationId: resolveScalerSarApplicationArn(architecture),
136
+ semanticVersion: BUILDKITE_STACK_PINS.scalerVersion,
137
+ parameters: {
138
+ BuildkiteAgentTokenParameter: config.agentTokenSsmParameterName,
139
+ ...(config.agentTokenKmsKeyArn !== undefined && {
140
+ BuildkiteAgentTokenParameterStoreKMSKey: config.agentTokenKmsKeyArn
141
+ }),
142
+ ...(config.rolePermissionsBoundaryArn !== undefined && {
143
+ RolePermissionsBoundaryARN: config.rolePermissionsBoundaryArn
144
+ }),
145
+ BuildkiteQueue: config.buildkiteQueue,
146
+ AgentsPerInstance: `${config.agentsPerInstance}`,
147
+ MinSize: `${config.agentMinInstances}`,
148
+ MaxSize: `${config.agentMaxInstances}`,
149
+ AgentAutoScaleGroup: autoScalingGroup.autoScalingGroupName,
150
+ ScaleOutFactor: config.scaleOutFactor,
151
+ ScaleOutForWaitingJobs: `${config.scaleOutWaitingForJobs}`,
152
+ EventSchedulePeriod: config.scalerEventSchedulePeriod,
153
+ MinPollInterval: config.scalerMinPollInterval,
154
+ LogRetentionDays: `${config.logRetentionDays}`,
155
+ // Pinned, not defaulted: the D7 scale-in posture above is only sound
156
+ // while the scaler never reduces DesiredCapacity. A scaler-side
157
+ // default flip must not change our termination semantics silently.
158
+ DisableScaleIn: "true"
159
+ },
160
+ costAllocationService: "buildkite",
161
+ costAllocationDomain: "buildkite-agent-scaler",
162
+ ...(config.costAllocationEnvironment !== undefined && {
163
+ costAllocationEnvironment: config.costAllocationEnvironment
164
+ })
165
+ });
166
+ addBuildkiteAlarms(this, {
167
+ buildkiteOrgSlug: config.buildkiteOrgSlug,
168
+ buildkiteQueue: config.buildkiteQueue,
169
+ autoScalingGroupName: autoScalingGroup.autoScalingGroupName,
170
+ ...(config.alarmSnsTopicArn !== undefined && {
171
+ alarmSnsTopicArn: config.alarmSnsTopicArn
172
+ })
173
+ });
174
+ this.artifactBucketName = artifactBucket.bucketName;
175
+ this.secretsBucketName = managedSecretsBucket.bucketName;
176
+ this.autoScalingGroupName = autoScalingGroup.autoScalingGroupName;
177
+ new CfnOutput(this, "BuildkiteQueueName", {
178
+ value: config.buildkiteQueue
179
+ });
180
+ new CfnOutput(this, "BuildkiteAgentAsgName", {
181
+ value: autoScalingGroup.autoScalingGroupName
182
+ });
183
+ new CfnOutput(this, "BuildkiteSecretsBucketName", {
184
+ value: managedSecretsBucket.bucketName
185
+ });
186
+ new CfnOutput(this, "BuildkiteArtifactBucketName", {
187
+ value: artifactBucket.bucketName
188
+ });
189
+ }
190
+ /**
191
+ * Ship the per-job `env` hook into the managed secrets bucket. The
192
+ * s3-secrets-hooks plugin sources this file at the start of EVERY job, so
193
+ * FJALL_API_KEY is read fresh from SSM per job — rotation takes effect on
194
+ * the next job with no instance replacement (design § D4(iii)). The script
195
+ * is configuration, not a secret: shipping it through a CDK asset is fine;
196
+ * the secret VALUE only ever moves SSM → instance at job runtime.
197
+ */
198
+ addFjallApiKeyEnvHook(id, secretsBucket, parameterName, region) {
199
+ new BucketDeployment(this, `${id}EnvHookDeployment`, {
200
+ sources: [
201
+ Source.data(FJALL_ENV_HOOK_OBJECT_KEY, buildFjallApiKeyEnvHookScript(parameterName, region))
202
+ ],
203
+ destinationBucket: secretsBucket,
204
+ // The secrets bucket also holds out-of-band objects the deployment
205
+ // does not know about (the git deploy key, design § D4(ii)). The CDK
206
+ // default `prune: true` DELETES every bucket object missing from
207
+ // `sources` on each custom-resource execution — wiping those secrets.
208
+ prune: false,
209
+ retainOnDelete: true
210
+ });
211
+ }
212
+ }
213
+ /**
214
+ * Object key the s3-secrets-hooks plugin sources at the start of every job —
215
+ * upstream contract: `env` at the secrets-bucket root.
216
+ */
217
+ export const FJALL_ENV_HOOK_OBJECT_KEY = "env";
218
+ /**
219
+ * The per-job env hook's script content (design § D4(iii)): FJALL_API_KEY is
220
+ * read fresh from SSM per job, so rotation takes effect on the next job with
221
+ * no instance replacement. Pure — the unit test pins the exact content.
222
+ */
223
+ export function buildFjallApiKeyEnvHookScript(parameterName, region) {
224
+ return [
225
+ `FJALL_API_KEY="$(aws ssm get-parameter --name '${parameterName}' --with-decryption --query Parameter.Value --output text --region '${region}')"`,
226
+ "export FJALL_API_KEY",
227
+ ""
228
+ ].join("\n");
229
+ }
230
+ function deriveCpuArchitecture(instanceTypeIdentifier) {
231
+ const architecture = new InstanceType(instanceTypeIdentifier).architecture;
232
+ return architecture === InstanceArchitecture.ARM_64 ? "arm64" : "amd64";
233
+ }
@@ -0,0 +1,35 @@
1
+ import type { Construct } from "constructs";
2
+ import { Role } from "../../../resources/aws/iam/index.js";
3
+ /**
4
+ * Every IAM action the agent instance profile is permitted to carry — the
5
+ * no-deploy-IAM invariant (design § D9). Deploy AWS credentials are
6
+ * server-minted (FJALL_API_KEY → Fjall OIDC → target-account role); the
7
+ * instance profile confers NO deploy capability. Explicitly absent: any
8
+ * `ecr:*` write, `sts:AssumeRole`, any CloudFormation mutation.
9
+ *
10
+ * The allowlist synth test asserts the synthesised role's actions are a
11
+ * subset of this list, so an upstream bump (or a future edit) that grows
12
+ * permissions fails loudly instead of shipping silently.
13
+ */
14
+ export declare const BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST: readonly ["ssm:GetParameter", "kms:Decrypt", "s3:GetObject", "s3:ListBucket", "s3:GetObjectVersion", "s3:PutObject", "s3:PutObjectAcl", "s3:PutObjectVersionAcl", "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogGroups", "logs:DescribeLogStreams", "logs:PutRetentionPolicy", "ssm:DescribeInstanceProperties", "ssm:ListAssociations", "ssm:PutInventory", "ssm:UpdateInstanceInformation", "ssmmessages:CreateControlChannel", "ssmmessages:CreateDataChannel", "ssmmessages:OpenControlChannel", "ssmmessages:OpenDataChannel", "ec2messages:AcknowledgeMessage", "ec2messages:DeleteMessage", "ec2messages:FailMessage", "ec2messages:GetEndpoint", "ec2messages:GetMessages", "ec2messages:SendReply", "autoscaling:DescribeAutoScalingInstances", "autoscaling:SetInstanceHealth", "autoscaling:TerminateInstanceInAutoScalingGroup", "cloudwatch:PutMetricData", "cloudformation:DescribeStackResource", "ec2:DescribeTags"];
15
+ export interface BuildkiteAgentRoleParams {
16
+ readonly agentTokenParameterArn: string;
17
+ readonly agentTokenKmsKeyArn?: string;
18
+ readonly fjallApiKeyParameterArn?: string;
19
+ readonly secretsBucketArn: string;
20
+ readonly artifactBucketArn: string;
21
+ /** Own-stack ARN — `cloudformation:DescribeStackResource` scope. */
22
+ readonly stackArn: string;
23
+ /** `arn:...:log-group:/buildkite/*` — the elastic stack's group namespace. */
24
+ readonly logGroupArnPattern: string;
25
+ }
26
+ /**
27
+ * Build the agent instance role. Tighter than the upstream v6.68.1 template
28
+ * on three axes: logs actions are scoped to the `/buildkite/*` group
29
+ * namespace (upstream: `*`), `cloudformation:DescribeStackResource` is
30
+ * scoped to the own stack (upstream: `*`), and there is no ECR/docker-login
31
+ * grant surface at all. The remaining `resources: ["*"]` statements are
32
+ * describe-only or instance-inventory actions with no resource-level
33
+ * support.
34
+ */
35
+ export declare function buildAgentRole(scope: Construct, id: string, params: BuildkiteAgentRoleParams): Role;
@@ -0,0 +1,176 @@
1
+ import { CompositePrincipal, PolicyDocument, PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
2
+ import { Role } from "../../../resources/aws/iam/index.js";
3
+ /**
4
+ * Every IAM action the agent instance profile is permitted to carry — the
5
+ * no-deploy-IAM invariant (design § D9). Deploy AWS credentials are
6
+ * server-minted (FJALL_API_KEY → Fjall OIDC → target-account role); the
7
+ * instance profile confers NO deploy capability. Explicitly absent: any
8
+ * `ecr:*` write, `sts:AssumeRole`, any CloudFormation mutation.
9
+ *
10
+ * The allowlist synth test asserts the synthesised role's actions are a
11
+ * subset of this list, so an upstream bump (or a future edit) that grows
12
+ * permissions fails loudly instead of shipping silently.
13
+ */
14
+ export const BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST = [
15
+ "ssm:GetParameter",
16
+ "kms:Decrypt",
17
+ "s3:GetObject",
18
+ "s3:ListBucket",
19
+ "s3:GetObjectVersion",
20
+ "s3:PutObject",
21
+ "s3:PutObjectAcl",
22
+ "s3:PutObjectVersionAcl",
23
+ "logs:CreateLogGroup",
24
+ "logs:CreateLogStream",
25
+ "logs:PutLogEvents",
26
+ "logs:DescribeLogGroups",
27
+ "logs:DescribeLogStreams",
28
+ "logs:PutRetentionPolicy",
29
+ "ssm:DescribeInstanceProperties",
30
+ "ssm:ListAssociations",
31
+ "ssm:PutInventory",
32
+ "ssm:UpdateInstanceInformation",
33
+ "ssmmessages:CreateControlChannel",
34
+ "ssmmessages:CreateDataChannel",
35
+ "ssmmessages:OpenControlChannel",
36
+ "ssmmessages:OpenDataChannel",
37
+ "ec2messages:AcknowledgeMessage",
38
+ "ec2messages:DeleteMessage",
39
+ "ec2messages:FailMessage",
40
+ "ec2messages:GetEndpoint",
41
+ "ec2messages:GetMessages",
42
+ "ec2messages:SendReply",
43
+ "autoscaling:DescribeAutoScalingInstances",
44
+ "autoscaling:SetInstanceHealth",
45
+ "autoscaling:TerminateInstanceInAutoScalingGroup",
46
+ "cloudwatch:PutMetricData",
47
+ "cloudformation:DescribeStackResource",
48
+ "ec2:DescribeTags"
49
+ ];
50
+ /**
51
+ * Build the agent instance role. Tighter than the upstream v6.68.1 template
52
+ * on three axes: logs actions are scoped to the `/buildkite/*` group
53
+ * namespace (upstream: `*`), `cloudformation:DescribeStackResource` is
54
+ * scoped to the own stack (upstream: `*`), and there is no ECR/docker-login
55
+ * grant surface at all. The remaining `resources: ["*"]` statements are
56
+ * describe-only or instance-inventory actions with no resource-level
57
+ * support.
58
+ */
59
+ export function buildAgentRole(scope, id, params) {
60
+ return new Role(scope, id, {
61
+ description: "Buildkite agent instance role — allowlist-only, no deploy capability",
62
+ inlinePolicies: buildAgentPolicyDocuments(params),
63
+ assumedBy: new CompositePrincipal(new ServicePrincipal("autoscaling.amazonaws.com"), new ServicePrincipal("ec2.amazonaws.com"))
64
+ });
65
+ }
66
+ function buildAgentPolicyDocuments(params) {
67
+ const documents = {
68
+ readAgentSecrets: new PolicyDocument({
69
+ statements: [
70
+ new PolicyStatement({
71
+ actions: ["ssm:GetParameter"],
72
+ resources: [
73
+ params.agentTokenParameterArn,
74
+ ...(params.fjallApiKeyParameterArn !== undefined
75
+ ? [params.fjallApiKeyParameterArn]
76
+ : [])
77
+ ]
78
+ }),
79
+ ...(params.agentTokenKmsKeyArn !== undefined
80
+ ? [
81
+ new PolicyStatement({
82
+ actions: ["kms:Decrypt"],
83
+ resources: [params.agentTokenKmsKeyArn]
84
+ })
85
+ ]
86
+ : [])
87
+ ]
88
+ }),
89
+ readManagedSecretsBucket: new PolicyDocument({
90
+ statements: [
91
+ new PolicyStatement({
92
+ actions: ["s3:GetObject", "s3:ListBucket"],
93
+ resources: [params.secretsBucketArn, `${params.secretsBucketArn}/*`]
94
+ })
95
+ ]
96
+ }),
97
+ artifactBucket: new PolicyDocument({
98
+ statements: [
99
+ new PolicyStatement({
100
+ // The Acl grants pair with the pinned BUILDKITE_S3_ACL env value:
101
+ // S3 evaluates s3:PutObjectAcl whenever a PutObject request carries
102
+ // an x-amz-acl header, so uploads fail AccessDenied without them.
103
+ actions: [
104
+ "s3:GetObject",
105
+ "s3:GetObjectVersion",
106
+ "s3:ListBucket",
107
+ "s3:PutObject",
108
+ "s3:PutObjectAcl",
109
+ "s3:PutObjectVersionAcl"
110
+ ],
111
+ resources: [params.artifactBucketArn, `${params.artifactBucketArn}/*`]
112
+ })
113
+ ]
114
+ }),
115
+ logging: new PolicyDocument({
116
+ statements: [
117
+ new PolicyStatement({
118
+ actions: [
119
+ "logs:CreateLogGroup",
120
+ "logs:CreateLogStream",
121
+ "logs:PutLogEvents",
122
+ "logs:PutRetentionPolicy"
123
+ ],
124
+ resources: [
125
+ params.logGroupArnPattern,
126
+ `${params.logGroupArnPattern}:*`
127
+ ]
128
+ }),
129
+ new PolicyStatement({
130
+ actions: ["logs:DescribeLogGroups", "logs:DescribeLogStreams"],
131
+ resources: ["*"]
132
+ })
133
+ ]
134
+ }),
135
+ describeInstance: new PolicyDocument({
136
+ statements: [
137
+ new PolicyStatement({
138
+ actions: [
139
+ "autoscaling:DescribeAutoScalingInstances",
140
+ "cloudwatch:PutMetricData",
141
+ "ec2:DescribeTags"
142
+ ],
143
+ resources: ["*"]
144
+ }),
145
+ new PolicyStatement({
146
+ actions: ["cloudformation:DescribeStackResource"],
147
+ resources: [params.stackArn]
148
+ })
149
+ ]
150
+ }),
151
+ sessionManager: new PolicyDocument({
152
+ statements: [
153
+ new PolicyStatement({
154
+ actions: [
155
+ "ssm:DescribeInstanceProperties",
156
+ "ssm:ListAssociations",
157
+ "ssm:PutInventory",
158
+ "ssm:UpdateInstanceInformation",
159
+ "ssmmessages:CreateControlChannel",
160
+ "ssmmessages:CreateDataChannel",
161
+ "ssmmessages:OpenControlChannel",
162
+ "ssmmessages:OpenDataChannel",
163
+ "ec2messages:AcknowledgeMessage",
164
+ "ec2messages:DeleteMessage",
165
+ "ec2messages:FailMessage",
166
+ "ec2messages:GetEndpoint",
167
+ "ec2messages:GetMessages",
168
+ "ec2messages:SendReply"
169
+ ],
170
+ resources: ["*"]
171
+ })
172
+ ]
173
+ })
174
+ };
175
+ return documents;
176
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Buildkite Elastic CI Stack version pins — the single moving-together
3
+ * contract for everything the `Buildkite` pattern derives from an upstream
4
+ * release. AMI ids, the elastic-stack version, the scaler version and the
5
+ * secrets-plugin floor MUST only change together, regenerated from the
6
+ * upstream template for the new version (runbook:
7
+ * `aiDocs/runbooks/buildkite-stack-bump-runbook.md`).
8
+ *
9
+ * AMI ids come from the `Mappings.AWSRegion2AMI` block of
10
+ * `https://s3.amazonaws.com/buildkite-aws-stack/<version>/aws-stack.yml` and
11
+ * are owner-verified against `ec2 describe-images` (owner MUST be
12
+ * `BUILDKITE_AMI_OWNER_ACCOUNT_ID`) before landing here. The vendored copy of
13
+ * the template lives at
14
+ * `lib/__tests__/fixtures/buildkite/upstream-aws-stack-<version>.yml` and
15
+ * backs the user-data parity test.
16
+ *
17
+ * Explicit ids rather than `MachineImage.lookup` — a name-pattern lookup
18
+ * floats the AMI underneath a pinned stack version, needs lookup credentials
19
+ * at synth time (unavailable in worker synth), and makes synth
20
+ * non-deterministic. Design:
21
+ * `aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md` § D2.
22
+ */
23
+ export declare const BUILDKITE_AMI_OWNER_ACCOUNT_ID = "172840064832";
24
+ export declare const BUILDKITE_CPU_ARCHITECTURES: readonly ["arm64", "amd64"];
25
+ export type BuildkiteCpuArchitecture = (typeof BUILDKITE_CPU_ARCHITECTURES)[number];
26
+ export declare const BUILDKITE_STACK_PINS: {
27
+ /** Upstream elastic-stack release the AMIs and user-data contract match. */
28
+ readonly elasticStackVersion: "v6.68.1";
29
+ /** buildkite-agent-scaler SAR semantic version. */
30
+ readonly scalerVersion: "1.12.0";
31
+ /**
32
+ * Minimum s3-secrets-hooks plugin version baked into the pinned AMIs.
33
+ * Versions bundled with elastic stack v6.41.0–6.41.3 leak secrets to the
34
+ * build log (GHSA fixed in 2.7.0); any bump below this floor must be
35
+ * rejected at review. v6.68.1 bundles >= 2.7.0.
36
+ */
37
+ readonly secretsPluginFloor: "2.7.0";
38
+ /**
39
+ * buildkite-agent-scaler SAR application ARNs per architecture, from the
40
+ * upstream template's `Mappings` block. The SAR publisher account is
41
+ * us-east-1-global; the ARN region does not constrain deploy region.
42
+ */
43
+ readonly scalerSarApplicationArns: {
44
+ readonly amd64: "arn:aws:serverlessrepo:us-east-1:172840064832:applications/buildkite-agent-scaler";
45
+ readonly arm64: "arn:aws:serverlessrepo:us-east-1:172840064832:applications/buildkite-agent-scaler-arm64";
46
+ };
47
+ /**
48
+ * Pinned AMI ids per region per architecture. Single-region today
49
+ * (us-east-1 — the fleet's home); add regions by regenerating from the
50
+ * upstream template, never by hand-picking an AMI. Owner-verified
51
+ * 2026-07-18: both ids owned by 172840064832, arm64 image
52
+ * `buildkite-stack-linux-arm64-2026-07-07T09-03-25Z`.
53
+ */
54
+ readonly amiIdsByRegion: {
55
+ readonly "us-east-1": {
56
+ readonly arm64: "ami-0ca21e2db030163c8";
57
+ readonly amd64: "ami-0a28a471ffade73ed";
58
+ };
59
+ };
60
+ };
61
+ export type BuildkiteSupportedRegion = keyof typeof BUILDKITE_STACK_PINS.amiIdsByRegion;
62
+ /**
63
+ * Resolve the pinned AMI id for a concrete region + architecture. Throws a
64
+ * synth-time error with the remediation path when the region is not pinned —
65
+ * adding a region is a pins regeneration, not a call-site workaround.
66
+ */
67
+ export declare function resolvePinnedAmiId(region: string, architecture: BuildkiteCpuArchitecture): string;
68
+ /** Resolve the architecture-matched buildkite-agent-scaler SAR ARN. */
69
+ export declare function resolveScalerSarApplicationArn(architecture: BuildkiteCpuArchitecture): string;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Buildkite Elastic CI Stack version pins — the single moving-together
3
+ * contract for everything the `Buildkite` pattern derives from an upstream
4
+ * release. AMI ids, the elastic-stack version, the scaler version and the
5
+ * secrets-plugin floor MUST only change together, regenerated from the
6
+ * upstream template for the new version (runbook:
7
+ * `aiDocs/runbooks/buildkite-stack-bump-runbook.md`).
8
+ *
9
+ * AMI ids come from the `Mappings.AWSRegion2AMI` block of
10
+ * `https://s3.amazonaws.com/buildkite-aws-stack/<version>/aws-stack.yml` and
11
+ * are owner-verified against `ec2 describe-images` (owner MUST be
12
+ * `BUILDKITE_AMI_OWNER_ACCOUNT_ID`) before landing here. The vendored copy of
13
+ * the template lives at
14
+ * `lib/__tests__/fixtures/buildkite/upstream-aws-stack-<version>.yml` and
15
+ * backs the user-data parity test.
16
+ *
17
+ * Explicit ids rather than `MachineImage.lookup` — a name-pattern lookup
18
+ * floats the AMI underneath a pinned stack version, needs lookup credentials
19
+ * at synth time (unavailable in worker synth), and makes synth
20
+ * non-deterministic. Design:
21
+ * `aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md` § D2.
22
+ */
23
+ export const BUILDKITE_AMI_OWNER_ACCOUNT_ID = "172840064832";
24
+ export const BUILDKITE_CPU_ARCHITECTURES = ["arm64", "amd64"];
25
+ export const BUILDKITE_STACK_PINS = {
26
+ /** Upstream elastic-stack release the AMIs and user-data contract match. */
27
+ elasticStackVersion: "v6.68.1",
28
+ /** buildkite-agent-scaler SAR semantic version. */
29
+ scalerVersion: "1.12.0",
30
+ /**
31
+ * Minimum s3-secrets-hooks plugin version baked into the pinned AMIs.
32
+ * Versions bundled with elastic stack v6.41.0–6.41.3 leak secrets to the
33
+ * build log (GHSA fixed in 2.7.0); any bump below this floor must be
34
+ * rejected at review. v6.68.1 bundles >= 2.7.0.
35
+ */
36
+ secretsPluginFloor: "2.7.0",
37
+ /**
38
+ * buildkite-agent-scaler SAR application ARNs per architecture, from the
39
+ * upstream template's `Mappings` block. The SAR publisher account is
40
+ * us-east-1-global; the ARN region does not constrain deploy region.
41
+ */
42
+ scalerSarApplicationArns: {
43
+ amd64: "arn:aws:serverlessrepo:us-east-1:172840064832:applications/buildkite-agent-scaler",
44
+ arm64: "arn:aws:serverlessrepo:us-east-1:172840064832:applications/buildkite-agent-scaler-arm64"
45
+ },
46
+ /**
47
+ * Pinned AMI ids per region per architecture. Single-region today
48
+ * (us-east-1 — the fleet's home); add regions by regenerating from the
49
+ * upstream template, never by hand-picking an AMI. Owner-verified
50
+ * 2026-07-18: both ids owned by 172840064832, arm64 image
51
+ * `buildkite-stack-linux-arm64-2026-07-07T09-03-25Z`.
52
+ */
53
+ amiIdsByRegion: {
54
+ "us-east-1": {
55
+ arm64: "ami-0ca21e2db030163c8",
56
+ amd64: "ami-0a28a471ffade73ed"
57
+ }
58
+ }
59
+ };
60
+ function isSupportedRegion(region) {
61
+ return region in BUILDKITE_STACK_PINS.amiIdsByRegion;
62
+ }
63
+ /**
64
+ * Resolve the pinned AMI id for a concrete region + architecture. Throws a
65
+ * synth-time error with the remediation path when the region is not pinned —
66
+ * adding a region is a pins regeneration, not a call-site workaround.
67
+ */
68
+ export function resolvePinnedAmiId(region, architecture) {
69
+ if (!isSupportedRegion(region)) {
70
+ throw new Error(`Buildkite: no pinned AMI for region '${region}'. Supported: ` +
71
+ `${Object.keys(BUILDKITE_STACK_PINS.amiIdsByRegion).join(", ")}. ` +
72
+ `Add the region to BUILDKITE_STACK_PINS via the bump runbook ` +
73
+ `(aiDocs/runbooks/buildkite-stack-bump-runbook.md).`);
74
+ }
75
+ return BUILDKITE_STACK_PINS.amiIdsByRegion[region][architecture];
76
+ }
77
+ /** Resolve the architecture-matched buildkite-agent-scaler SAR ARN. */
78
+ export function resolveScalerSarApplicationArn(architecture) {
79
+ return BUILDKITE_STACK_PINS.scalerSarApplicationArns[architecture];
80
+ }