@fjall/components-infrastructure 3.0.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.
@@ -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
+ }
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Plain-data props for the `Buildkite` pattern, validated at construct time.
4
+ * Deliberately CDK-free so the same schema can later back the customer
5
+ * scaffold surface if the pattern is ever promoted to `PATTERN_TYPE_VALUES`
6
+ * (design § D3 — promotion must be additive).
7
+ *
8
+ * Defaults encode the design's deploy-fleet posture
9
+ * (`aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`):
10
+ * on-demand-only (`spotCapacityPercentage` 0 — ASG mixed-instances has no
11
+ * spot→on-demand fallback, and a spot reclaim mid-deploy is a rollback
12
+ * incident, § D7), scale-to-zero, whole-instance agents, bounded instance
13
+ * staleness, and no ECR / docker-login instance-level credentials (§ D9 —
14
+ * deploy AWS access stays server-minted; the instance profile confers none).
15
+ */
16
+ export declare const BuildkitePropsSchema: z.ZodObject<{
17
+ buildkiteQueue: z.ZodString;
18
+ buildkiteOrgSlug: z.ZodString;
19
+ agentTokenSsmParameterName: z.ZodString;
20
+ agentTokenKmsKeyArn: z.ZodOptional<z.ZodString>;
21
+ fjallApiKeySsmParameterName: z.ZodOptional<z.ZodString>;
22
+ instanceType: z.ZodDefault<z.ZodString>;
23
+ agentVolumeSizeGib: z.ZodDefault<z.ZodNumber>;
24
+ agentMinInstances: z.ZodDefault<z.ZodNumber>;
25
+ agentMaxInstances: z.ZodDefault<z.ZodNumber>;
26
+ agentsPerInstance: z.ZodDefault<z.ZodNumber>;
27
+ spotCapacityPercentage: z.ZodDefault<z.ZodNumber>;
28
+ scaleInIdlePeriodSeconds: z.ZodDefault<z.ZodNumber>;
29
+ disconnectAfterUptimeSeconds: z.ZodDefault<z.ZodNumber>;
30
+ maxInstanceLifetimeDays: z.ZodDefault<z.ZodNumber>;
31
+ terminateInstanceAfterJob: z.ZodDefault<z.ZodBoolean>;
32
+ purgeBuildsOnDiskFull: z.ZodDefault<z.ZodBoolean>;
33
+ terminateInstanceOnDiskFull: z.ZodDefault<z.ZodBoolean>;
34
+ logRetentionDays: z.ZodDefault<z.ZodNumber>;
35
+ buildkiteAgentRelease: z.ZodDefault<z.ZodEnum<{
36
+ stable: "stable";
37
+ beta: "beta";
38
+ edge: "edge";
39
+ }>>;
40
+ buildkiteAgentTags: z.ZodDefault<z.ZodString>;
41
+ buildkiteAgentTimestampLines: z.ZodDefault<z.ZodBoolean>;
42
+ buildkiteAgentExperiments: z.ZodDefault<z.ZodString>;
43
+ buildkiteAgentTracingBackend: z.ZodDefault<z.ZodEnum<{
44
+ "": "";
45
+ datadog: "datadog";
46
+ opentelemetry: "opentelemetry";
47
+ }>>;
48
+ buildkiteAgentCancelGracePeriodSeconds: z.ZodDefault<z.ZodNumber>;
49
+ enableSecretsPlugin: z.ZodDefault<z.ZodBoolean>;
50
+ enableEcrPlugin: z.ZodDefault<z.ZodBoolean>;
51
+ enableDockerLoginPlugin: z.ZodDefault<z.ZodBoolean>;
52
+ enableDockerUserNamespaceRemap: z.ZodDefault<z.ZodBoolean>;
53
+ enableDockerExperimental: z.ZodDefault<z.ZodBoolean>;
54
+ dockerNetworkingProtocol: z.ZodDefault<z.ZodEnum<{
55
+ ipv4: "ipv4";
56
+ dualstack: "dualstack";
57
+ }>>;
58
+ enableInstanceStorage: z.ZodDefault<z.ZodBoolean>;
59
+ mountTmpfsAtTmp: z.ZodDefault<z.ZodBoolean>;
60
+ buildkiteAgentEnableGitMirrors: z.ZodDefault<z.ZodBoolean>;
61
+ bootstrapScriptUrl: z.ZodDefault<z.ZodString>;
62
+ agentEnvFileUrl: z.ZodDefault<z.ZodString>;
63
+ scalerEventSchedulePeriod: z.ZodDefault<z.ZodString>;
64
+ scalerMinPollInterval: z.ZodDefault<z.ZodString>;
65
+ scaleOutFactor: z.ZodDefault<z.ZodString>;
66
+ scaleOutWaitingForJobs: z.ZodDefault<z.ZodBoolean>;
67
+ rolePermissionsBoundaryArn: z.ZodOptional<z.ZodString>;
68
+ alarmSnsTopicArn: z.ZodOptional<z.ZodString>;
69
+ costAllocationEnvironment: z.ZodOptional<z.ZodString>;
70
+ costAllocationOwner: z.ZodOptional<z.ZodString>;
71
+ }, z.core.$strict>;
72
+ export type BuildkiteProps = z.infer<typeof BuildkitePropsSchema>;
73
+ /** Caller-facing shape: fields with defaults are optional at the call site. */
74
+ export type BuildkitePropsInput = z.input<typeof BuildkitePropsSchema>;
75
+ /**
76
+ * Validate + default the plain-data props at construct time. Throws a
77
+ * synth-time error listing every violation — the pattern's constructor is the
78
+ * validation boundary, mirroring `ClickHouseDatabase`'s Stage-1 shape.
79
+ */
80
+ export declare function validateBuildkiteProps(props: BuildkitePropsInput): BuildkiteProps;
@@ -0,0 +1,148 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Plain-data props for the `Buildkite` pattern, validated at construct time.
4
+ * Deliberately CDK-free so the same schema can later back the customer
5
+ * scaffold surface if the pattern is ever promoted to `PATTERN_TYPE_VALUES`
6
+ * (design § D3 — promotion must be additive).
7
+ *
8
+ * Defaults encode the design's deploy-fleet posture
9
+ * (`aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`):
10
+ * on-demand-only (`spotCapacityPercentage` 0 — ASG mixed-instances has no
11
+ * spot→on-demand fallback, and a spot reclaim mid-deploy is a rollback
12
+ * incident, § D7), scale-to-zero, whole-instance agents, bounded instance
13
+ * staleness, and no ECR / docker-login instance-level credentials (§ D9 —
14
+ * deploy AWS access stays server-minted; the instance profile confers none).
15
+ */
16
+ export const BuildkitePropsSchema = z
17
+ .object({
18
+ /** Buildkite queue this fleet serves (cluster-scoped via the token). */
19
+ buildkiteQueue: z
20
+ .string()
21
+ .min(1, "buildkiteQueue cannot be empty")
22
+ .max(100)
23
+ .regex(/^[a-zA-Z0-9-_]+$/, "buildkiteQueue must be alphanumeric with hyphens/underscores"),
24
+ /**
25
+ * Buildkite organisation slug (e.g. `fjall-tech`). The
26
+ * buildkite-agent-scaler publishes its CloudWatch metrics dimensioned by
27
+ * {Org, Queue} — the fleet's alarms must query the same pair or they read
28
+ * no data at all (heartbeat permanently ALARM, queued-with-zero-capacity
29
+ * permanently inert).
30
+ */
31
+ buildkiteOrgSlug: z
32
+ .string()
33
+ .min(1, "buildkiteOrgSlug cannot be empty")
34
+ .max(100)
35
+ .regex(/^[a-z0-9-]+$/, "buildkiteOrgSlug must be a lowercase Buildkite organisation slug"),
36
+ /**
37
+ * Name of the pre-provisioned SSM SecureString holding the cluster-scoped
38
+ * agent token (e.g. `/Buildkite/agents/agent-token`). Provisioned
39
+ * out-of-band via `fjall secrets` tooling — the construct receives the
40
+ * identifier only; no secret value ever transits synth (design § D4).
41
+ */
42
+ agentTokenSsmParameterName: z
43
+ .string()
44
+ .min(2)
45
+ .regex(/^\//, "agentTokenSsmParameterName must be a full path (leading /)"),
46
+ /**
47
+ * KMS key ARN encrypting the agent-token parameter, when a customer
48
+ * managed key is used. Omit for the AWS-managed `aws/ssm` key.
49
+ */
50
+ agentTokenKmsKeyArn: z.string().min(1).optional(),
51
+ /**
52
+ * Name of the SSM SecureString holding FJALL_API_KEY. When set, the
53
+ * construct ships a per-job `env` hook into the managed secrets bucket
54
+ * that reads the parameter at job start (rotation takes effect on the
55
+ * next job, no instance replacement — design § D4(iii)) and grants the
56
+ * instance profile read on exactly this parameter.
57
+ */
58
+ fjallApiKeySsmParameterName: z
59
+ .string()
60
+ .min(2)
61
+ .regex(/^\//, "fjallApiKeySsmParameterName must be a full path (leading /)")
62
+ .optional(),
63
+ /** EC2 instance type. Graviton default per design § D7. */
64
+ instanceType: z.string().min(1).default("c8g.xlarge"),
65
+ agentVolumeSizeGib: z.number().int().min(20).max(1000).default(250),
66
+ agentMinInstances: z.number().int().min(0).default(0),
67
+ agentMaxInstances: z.number().int().min(1).default(2),
68
+ agentsPerInstance: z.number().int().min(1).default(1),
69
+ /**
70
+ * Percentage of capacity on spot. Deploy fleets MUST stay 0 (on-demand
71
+ * only): no spot→on-demand fallback mechanism exists, and a reclaim
72
+ * mid-deploy lands in the rollback-wedge class (design § D7). Non-zero is
73
+ * for Phase-2 CI fleets running idempotent, auto-retried jobs.
74
+ */
75
+ spotCapacityPercentage: z.number().int().min(0).max(100).default(0),
76
+ /** Agent-driven scale-in: idle seconds before an instance self-terminates. */
77
+ scaleInIdlePeriodSeconds: z.number().int().min(60).default(600),
78
+ /** Bounded instance staleness; also caps stale-token propagation (§ D5). */
79
+ disconnectAfterUptimeSeconds: z.number().int().min(3600).default(86_400),
80
+ maxInstanceLifetimeDays: z.number().int().min(1).max(365).default(7),
81
+ terminateInstanceAfterJob: z.boolean().default(false),
82
+ purgeBuildsOnDiskFull: z.boolean().default(true),
83
+ terminateInstanceOnDiskFull: z.boolean().default(false),
84
+ /** CloudWatch retention for the scaler's log group (days). */
85
+ logRetentionDays: z.number().int().min(1).default(30),
86
+ buildkiteAgentRelease: z.enum(["stable", "beta", "edge"]).default("stable"),
87
+ buildkiteAgentTags: z.string().default(""),
88
+ buildkiteAgentTimestampLines: z.boolean().default(false),
89
+ buildkiteAgentExperiments: z.string().default(""),
90
+ buildkiteAgentTracingBackend: z
91
+ .enum(["", "datadog", "opentelemetry"])
92
+ .default(""),
93
+ buildkiteAgentCancelGracePeriodSeconds: z
94
+ .number()
95
+ .int()
96
+ .min(10)
97
+ .default(60),
98
+ /** S3 secrets-hooks plugin — required for the FJALL_API_KEY env hook. */
99
+ enableSecretsPlugin: z.boolean().default(true),
100
+ /**
101
+ * ECR + docker-login plugins default OFF: the instance profile carries no
102
+ * registry credentials (design § D9); deploy jobs authenticate through
103
+ * server-minted credentials exactly as on hosted agents.
104
+ */
105
+ enableEcrPlugin: z.boolean().default(false),
106
+ enableDockerLoginPlugin: z.boolean().default(false),
107
+ enableDockerUserNamespaceRemap: z.boolean().default(true),
108
+ enableDockerExperimental: z.boolean().default(false),
109
+ dockerNetworkingProtocol: z.enum(["ipv4", "dualstack"]).default("ipv4"),
110
+ enableInstanceStorage: z.boolean().default(false),
111
+ mountTmpfsAtTmp: z.boolean().default(true),
112
+ buildkiteAgentEnableGitMirrors: z.boolean().default(false),
113
+ bootstrapScriptUrl: z.string().default(""),
114
+ agentEnvFileUrl: z.string().default(""),
115
+ scalerEventSchedulePeriod: z.string().min(1).default("1 minute"),
116
+ scalerMinPollInterval: z.string().min(1).default("10s"),
117
+ scaleOutFactor: z.string().min(1).default("1.0"),
118
+ scaleOutWaitingForJobs: z.boolean().default(false),
119
+ /** IAM permissions boundary for the scaler's roles; emitted only when set. */
120
+ rolePermissionsBoundaryArn: z.string().min(1).optional(),
121
+ /**
122
+ * SNS topic receiving the fleet's two alarms (scaler heartbeat + queued
123
+ * with zero capacity — design § D14). Omit to create the alarms without
124
+ * actions (visible on the console, silent).
125
+ */
126
+ alarmSnsTopicArn: z.string().min(1).optional(),
127
+ costAllocationEnvironment: z.string().min(1).optional(),
128
+ costAllocationOwner: z.string().min(1).optional()
129
+ })
130
+ .strict()
131
+ .refine((props) => props.agentMinInstances <= props.agentMaxInstances, {
132
+ message: "agentMinInstances must be <= agentMaxInstances"
133
+ });
134
+ /**
135
+ * Validate + default the plain-data props at construct time. Throws a
136
+ * synth-time error listing every violation — the pattern's constructor is the
137
+ * validation boundary, mirroring `ClickHouseDatabase`'s Stage-1 shape.
138
+ */
139
+ export function validateBuildkiteProps(props) {
140
+ const result = BuildkitePropsSchema.safeParse(props);
141
+ if (!result.success) {
142
+ const details = result.error.issues
143
+ .map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`)
144
+ .join("\n");
145
+ throw new Error(`Buildkite: invalid props:\n${details}`);
146
+ }
147
+ return result.data;
148
+ }
@@ -0,0 +1,58 @@
1
+ import { MultipartUserData } from "aws-cdk-lib/aws-ec2";
2
+ import type { BuildkiteProps } from "./schema.js";
3
+ /**
4
+ * Resolved synth-time values the user data needs beyond the plain props —
5
+ * supplied by the construct (region from `Stack.of(this)`, never
6
+ * `props.env?.region`, which rendered the literal string "undefined" in the
7
+ * pre-refactor stack).
8
+ */
9
+ export interface BuildkiteUserDataContext {
10
+ readonly stackName: string;
11
+ readonly region: string;
12
+ readonly secretsBucketName: string;
13
+ readonly artifactBucketName: string;
14
+ }
15
+ /**
16
+ * The pinned AMI's boot scripts run under `set -Eeuo pipefail` and
17
+ * dereference every variable the upstream template passes — an OMITTED key
18
+ * is not "use the default", it is an unbound-variable crash at boot and an
19
+ * ASG boot-loop. Each part therefore mirrors the upstream v6.68.1 user data
20
+ * env block EXACTLY (same keys, upstream parameter defaults where we expose
21
+ * no knob), and the parity test asserts set equality per part against the
22
+ * vendored template in both directions: a key we emit that upstream does not
23
+ * pass is silently ignored; a key upstream passes that we omit is the
24
+ * boot-loop. Bumping the pins regenerates the fixture
25
+ * (runbook: `aiDocs/runbooks/buildkite-stack-bump-runbook.md`).
26
+ */
27
+ export declare const BUILDKITE_MOUNT_PART_ENV_KEYS: readonly ["BUILDKITE_ENABLE_INSTANCE_STORAGE", "BUILDKITE_MOUNT_TMPFS_AT_TMP"];
28
+ export declare const BUILDKITE_DOCKER_PART_ENV_KEYS: readonly ["DOCKER_USERNS_REMAP", "DOCKER_EXPERIMENTAL", "DOCKER_PRUNE_UNTIL", "ENABLE_PRE_EXIT_DISK_CLEANUP", "DOCKER_BUILDER_PRUNE_ENABLED", "DOCKER_NETWORKING_PROTOCOL", "DOCKER_IPV4_ADDRESS_POOL_1", "DOCKER_IPV4_ADDRESS_POOL_2", "DOCKER_IPV6_ADDRESS_POOL", "DOCKER_FIXED_CIDR_V4", "DOCKER_FIXED_CIDR_V6", "BUILDKITE_ENABLE_INSTANCE_STORAGE"];
29
+ export declare const BUILDKITE_USER_DATA_ENV_KEYS: readonly ["BUILDKITE_STACK_NAME", "BUILDKITE_STACK_VERSION", "BUILDKITE_STACK_DEPLOYED_BY", "BUILDKITE_SCALE_IN_IDLE_PERIOD", "BUILDKITE_SECRETS_BUCKET", "BUILDKITE_SECRETS_BUCKET_REGION", "BUILDKITE_SECRETS_PLUGIN_SKIP_SSH_KEY_NOT_FOUND_WARNING", "BUILDKITE_ARTIFACTS_BUCKET", "BUILDKITE_S3_DEFAULT_REGION", "BUILDKITE_S3_ACL", "BUILDKITE_AGENT_TOKEN_PATH", "BUILDKITE_AGENTS_PER_INSTANCE", "BUILDKITE_AGENT_ENDPOINT", "BUILDKITE_AGENT_TAGS", "BUILDKITE_AGENT_TIMESTAMP_LINES", "BUILDKITE_AGENT_EXPERIMENTS", "BUILDKITE_AGENT_TRACING_BACKEND", "BUILDKITE_AGENT_SIGNING_KEY_PATH", "BUILDKITE_AGENT_SIGNING_KEY_ID", "BUILDKITE_AGENT_VERIFICATION_KEY_PATH", "BUILDKITE_AGENT_RELEASE", "BUILDKITE_AGENT_CANCEL_GRACE_PERIOD", "BUILDKITE_AGENT_SIGNAL_GRACE_PERIOD_SECONDS", "BUILDKITE_AGENT_SIGNING_KMS_KEY", "BUILDKITE_AGENT_JOB_VERIFICATION_NO_SIGNATURE_BEHAVIOR", "BUILDKITE_QUEUE", "BUILDKITE_AGENT_ENABLE_GIT_MIRRORS", "BUILDKITE_ELASTIC_BOOTSTRAP_SCRIPT", "BUILDKITE_ENV_FILE_URL", "BUILDKITE_ENABLE_INSTANCE_STORAGE", "BUILDKITE_AUTHORIZED_USERS_URL", "BUILDKITE_ECR_POLICY", "BUILDKITE_TERMINATE_INSTANCE_AFTER_JOB", "BUILDKITE_AGENT_DISCONNECT_AFTER_UPTIME", "BUILDKITE_TERMINATE_INSTANCE_ON_DISK_FULL", "BUILDKITE_PURGE_BUILDS_ON_DISK_FULL", "BUILDKITE_ADDITIONAL_SUDO_PERMISSIONS", "AWS_DEFAULT_REGION", "SECRETS_PLUGIN_ENABLED", "ECR_PLUGIN_ENABLED", "ECR_CREDENTIAL_HELPER_ENABLED", "DOCKER_LOGIN_PLUGIN_ENABLED", "DOCKER_EXPERIMENTAL", "DOCKER_PRUNE_UNTIL", "ENABLE_PRE_EXIT_DISK_CLEANUP", "DOCKER_BUILDER_PRUNE_ENABLED", "DOCKER_USERNS_REMAP", "AWS_REGION", "ENABLE_RESOURCE_LIMITS", "RESOURCE_LIMITS_MEMORY_HIGH", "RESOURCE_LIMITS_MEMORY_MAX", "RESOURCE_LIMITS_MEMORY_SWAP_MAX", "RESOURCE_LIMITS_CPU_WEIGHT", "RESOURCE_LIMITS_CPU_QUOTA", "RESOURCE_LIMITS_IO_WEIGHT", "ENABLE_EC2_LOG_RETENTION_POLICY", "EC2_LOG_RETENTION_DAYS"];
30
+ /**
31
+ * Build the agent instance's multipart user data against the pinned elastic
32
+ * stack's boot scripts (cloud-config re-run marker, storage mount, docker
33
+ * config, `bk-install-elastic-stack.sh`).
34
+ */
35
+ export declare function buildBuildkiteUserData(props: BuildkiteProps, context: BuildkiteUserDataContext): MultipartUserData;
36
+ /** Storage-mount part — `bk-mount-instance-storage.sh` env assignments. */
37
+ export declare function buildBuildkiteMountCommands(props: BuildkiteProps): string[];
38
+ /** Docker-configure part — `bk-configure-docker.sh` env assignments. */
39
+ export declare function buildBuildkiteDockerCommands(props: BuildkiteProps): string[];
40
+ /**
41
+ * Install part — the env assignments handed to the pinned
42
+ * `bk-install-elastic-stack.sh`, mirroring the upstream block key-for-key in
43
+ * upstream order (so a fixture-bump diff reads line-by-line). Keys with no
44
+ * schema knob carry the upstream v6.68.1 parameter default verbatim, with
45
+ * three deliberate deviations:
46
+ *
47
+ * - `BUILDKITE_S3_ACL`: our artifact bucket is BucketOwnerEnforced (ACLs
48
+ * disabled), where S3 rejects every canned ACL EXCEPT
49
+ * `bucket-owner-full-control` — upstream's `private` default would fail
50
+ * every artifact upload with AccessControlListNotSupported.
51
+ * - `BUILDKITE_ECR_POLICY`: hard-pinned `none` (design § D9 — the instance
52
+ * profile confers no registry credentials).
53
+ * - `BUILDKITE_AGENT_SIGNING_KMS_KEY`: hard-pinned empty. Pipeline signing
54
+ * is out of scope for the Phase-1 deploy fleet; a future signing knob must
55
+ * land WITH the kms:Sign/Verify/GetPublicKey grants it needs (the prior
56
+ * knob shipped without them — a dead toggle that broke agents when set).
57
+ */
58
+ export declare function buildBuildkiteInstallCommands(props: BuildkiteProps, context: BuildkiteUserDataContext): string[];