@fjall/components-infrastructure 3.4.1 → 3.6.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.
- package/dist/lib/patterns/aws/apexDomainPattern.js +12 -24
- package/dist/lib/patterns/aws/buildkite/buildkite.d.ts +54 -9
- package/dist/lib/patterns/aws/buildkite/buildkite.js +131 -23
- package/dist/lib/patterns/aws/buildkite/iam.d.ts +8 -1
- package/dist/lib/patterns/aws/buildkite/iam.js +11 -0
- package/dist/lib/patterns/aws/buildkite/userData.d.ts +13 -1
- package/dist/lib/patterns/aws/buildkite/userData.js +8 -3
- package/dist/lib/patterns/aws/buildkite.d.ts +1 -1
- package/dist/lib/patterns/aws/buildkite.js +1 -1
- package/dist/lib/patterns/aws/database.d.ts +6 -0
- package/dist/lib/patterns/aws/database.js +2 -1
- package/dist/lib/patterns/aws/delegatedDomainPattern.d.ts +6 -2
- package/dist/lib/patterns/aws/delegatedDomainPattern.js +28 -27
- package/dist/lib/patterns/aws/devSubstrate.js +15 -5
- package/dist/lib/patterns/aws/domainCertificateComposer.d.ts +48 -0
- package/dist/lib/patterns/aws/domainCertificateComposer.js +179 -0
- package/dist/lib/patterns/aws/domainValidation.js +95 -7
- package/dist/lib/patterns/aws/interfaces/domain.d.ts +33 -0
- package/dist/lib/resources/aws/compute/ecsRoles.d.ts +7 -0
- package/dist/lib/resources/aws/compute/ecsRoles.js +8 -1
- package/dist/lib/resources/aws/database/rdsAurora.d.ts +7 -0
- package/dist/lib/resources/aws/database/rdsAurora.js +2 -1
- package/dist/lib/resources/aws/database/rdsDefaults.d.ts +6 -3
- package/dist/lib/resources/aws/database/rdsDefaults.js +6 -3
- package/dist/lib/resources/aws/networking/crossAccountDelegationRecord.d.ts +10 -0
- package/dist/lib/resources/aws/networking/crossAccountDelegationRecord.js +27 -1
- package/dist/lib/resources/aws/secrets/secret.d.ts +6 -0
- package/dist/lib/resources/aws/secrets/secret.js +1 -1
- package/dist/lib/utils/domainTypes.d.ts +5 -11
- package/dist/lib/utils/domainTypes.js +7 -4
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HostedZone } from "../../resources/aws/networking/hostedZone.js";
|
|
2
|
-
import {
|
|
2
|
+
import { composeDomainCertificates } from "./domainCertificateComposer.js";
|
|
3
3
|
import { composeTypedDnsRecords } from "./dnsRecordComposer.js";
|
|
4
4
|
import { toPascalCase, getSafeZoneName } from "../../utils/capitaliseString.js";
|
|
5
5
|
/**
|
|
@@ -27,19 +27,17 @@ export function composeApexDomain(scope, props) {
|
|
|
27
27
|
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
28
28
|
costAllocationDomain: props.zoneName
|
|
29
29
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
});
|
|
42
|
-
certificates.set(normalised.domainName, dc.certificate);
|
|
30
|
+
// Regional entries mint beside the zone as before; cloudFront entries
|
|
31
|
+
// mint in us-east-1 (in-stack here, or in the domain-paired
|
|
32
|
+
// UsEast1Certificates stack) and publish the zone-level export (D3).
|
|
33
|
+
const certificates = composeDomainCertificates(scope, {
|
|
34
|
+
certificates: props.certificates,
|
|
35
|
+
effectiveZoneName: props.zoneName,
|
|
36
|
+
safeZone,
|
|
37
|
+
hostedZone: hostedZoneConstruct.hostedZone,
|
|
38
|
+
hostedZoneId: props.hostedZoneId,
|
|
39
|
+
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
40
|
+
costAllocationDomain: props.zoneName
|
|
43
41
|
});
|
|
44
42
|
if (props.records && props.records.length > 0) {
|
|
45
43
|
composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, props.zoneName, props.records);
|
|
@@ -55,13 +53,3 @@ export function composeApexDomain(scope, props) {
|
|
|
55
53
|
manualRecords: []
|
|
56
54
|
};
|
|
57
55
|
}
|
|
58
|
-
function normaliseCertificate(cert) {
|
|
59
|
-
if (typeof cert === "string") {
|
|
60
|
-
return { domainName: cert };
|
|
61
|
-
}
|
|
62
|
-
return {
|
|
63
|
-
domainName: cert.domainName,
|
|
64
|
-
subjectAlternativeNames: cert.subjectAlternativeNames,
|
|
65
|
-
transparencyLogging: cert.transparencyLogging
|
|
66
|
-
};
|
|
67
|
-
}
|
|
@@ -23,15 +23,24 @@ export declare class Buildkite extends Construct {
|
|
|
23
23
|
readonly secretsBucketName: string;
|
|
24
24
|
readonly autoScalingGroupName: string;
|
|
25
25
|
constructor(scope: Construct, id: string, props: BuildkiteConstructProps);
|
|
26
|
+
/**
|
|
27
|
+
* Derive an agent-cluster secret path from the app namespace — the same
|
|
28
|
+
* `buildParameterPath` derivation `fjall secrets` and the ECS pattern use,
|
|
29
|
+
* so derived parameters stay manageable by the shared tooling. Explicit
|
|
30
|
+
* props bypass this entirely; it throws only when derivation is the last
|
|
31
|
+
* resort and no valid `applicationId` exists to derive from.
|
|
32
|
+
*/
|
|
33
|
+
private deriveAgentSecretPath;
|
|
26
34
|
/**
|
|
27
35
|
* Ship the per-job `env` hook into the managed secrets bucket. The
|
|
28
36
|
* s3-secrets-hooks plugin sources this file at the start of EVERY job, so
|
|
29
|
-
* FJALL_API_KEY
|
|
30
|
-
* the next job with no instance
|
|
31
|
-
*
|
|
32
|
-
*
|
|
37
|
+
* FJALL_API_KEY and the per-pipeline job secrets are read fresh from SSM
|
|
38
|
+
* per job — rotation takes effect on the next job with no instance
|
|
39
|
+
* replacement (design § D4(iii)). The script is configuration, not a
|
|
40
|
+
* secret: shipping it through a CDK asset is fine; secret VALUES only ever
|
|
41
|
+
* move SSM → instance at job runtime.
|
|
33
42
|
*/
|
|
34
|
-
private
|
|
43
|
+
private addAgentSecretsEnvHook;
|
|
35
44
|
}
|
|
36
45
|
/**
|
|
37
46
|
* Object key the s3-secrets-hooks plugin sources at the start of every job —
|
|
@@ -39,8 +48,44 @@ export declare class Buildkite extends Construct {
|
|
|
39
48
|
*/
|
|
40
49
|
export declare const FJALL_ENV_HOOK_OBJECT_KEY = "env";
|
|
41
50
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
51
|
+
* Cluster segment of derived agent secret paths
|
|
52
|
+
* (`/<applicationId>/agents/<name>`) — the namespace the live estate already
|
|
53
|
+
* uses for its explicitly-provisioned parameters.
|
|
54
|
+
*/
|
|
55
|
+
export declare const BUILDKITE_SECRETS_CLUSTER = "agents";
|
|
56
|
+
/** Derived secret name for the cluster-scoped Buildkite agent token. */
|
|
57
|
+
export declare const AGENT_TOKEN_SECRET_NAME = "agent-token";
|
|
58
|
+
/** Derived secret name for the FJALL_API_KEY env-hook parameter. */
|
|
59
|
+
export declare const FJALL_API_KEY_SECRET_NAME = "fjall-api-key";
|
|
60
|
+
/** Inputs to {@link buildAgentSecretsEnvHookScript}; omitted members skip their block. */
|
|
61
|
+
export interface AgentSecretsEnvHookOptions {
|
|
62
|
+
/** Resolved FJALL_API_KEY parameter path (explicit prop or derived). */
|
|
63
|
+
readonly fjallApiKeyParameterName?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Namespace root for per-pipeline job secrets (`/<applicationId>/agents`);
|
|
66
|
+
* the hook lists `<prefix>/<BUILDKITE_PIPELINE_SLUG>` per job. Jobs read
|
|
67
|
+
* the whole `/<app>/agents/*` subtree via the paired IAM grant — identical
|
|
68
|
+
* flatness to the previous whole-bucket S3 read; isolation stays per-fleet.
|
|
69
|
+
*/
|
|
70
|
+
readonly pipelineSecretsPrefix?: string;
|
|
71
|
+
readonly region: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The per-job env hook's script content (design § D4(iii) + Phase B):
|
|
75
|
+
* FJALL_API_KEY and the per-pipeline job secrets are read fresh from SSM per
|
|
76
|
+
* job, so rotation takes effect on the next job with no instance replacement.
|
|
77
|
+
* Pure — the unit tests byte-pin both emitted variants and `sh -n` them.
|
|
78
|
+
*
|
|
79
|
+
* The script is eval'd by the s3secrets plugin under bash
|
|
80
|
+
* `set -Eeuo pipefail` + an ERR trap that exits 53 (fails the job) +
|
|
81
|
+
* `set -o allexport` (every assignment auto-exports) + `nounset` — pinned in
|
|
82
|
+
* `lib/__tests__/fixtures/buildkite/upstream-hooks-v6.68.1/`. Hence: every
|
|
83
|
+
* fallible command is guarded, every env read uses `${VAR:-}`, every temp
|
|
84
|
+
* variable is unset after use, and the script never exits. `private_ssh_key`
|
|
85
|
+
* loads into an ephemeral ssh-agent (stdin → agent memory, never disk or
|
|
86
|
+
* argv); the upstream pre-exit hook kills whatever `SSH_AGENT_PID` is
|
|
87
|
+
* exported, so our agent's lifecycle is owned upstream. Every fetched value
|
|
88
|
+
* is registered with the agent redactor (stdin form) before use — the
|
|
89
|
+
* upstream helper cannot see values we fetch at eval time.
|
|
45
90
|
*/
|
|
46
|
-
export declare function
|
|
91
|
+
export declare function buildAgentSecretsEnvHookScript(options: AgentSecretsEnvHookOptions): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CfnOutput, Duration, RemovalPolicy, Stack, Token } from "aws-cdk-lib";
|
|
1
|
+
import { Annotations, CfnOutput, Duration, RemovalPolicy, Stack, Token } from "aws-cdk-lib";
|
|
2
|
+
import { SSM_COMPONENT_PATTERN, buildParameterPath } from "@fjall/util";
|
|
2
3
|
import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
|
|
3
4
|
import { InstanceArchitecture, InstanceType, MachineImage, SubnetType } from "aws-cdk-lib/aws-ec2";
|
|
4
5
|
import { PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
@@ -38,6 +39,37 @@ export class Buildkite extends Construct {
|
|
|
38
39
|
super(scope, id);
|
|
39
40
|
const { vpc, ...plainProps } = props;
|
|
40
41
|
const config = validateBuildkiteProps(plainProps);
|
|
42
|
+
// Deliberate asymmetry: the agent token is load-bearing (boot + scaler),
|
|
43
|
+
// so underivable throws; the api-key hook and per-pipeline job secrets
|
|
44
|
+
// are capabilities, so underivable — including a pattern-invalid
|
|
45
|
+
// applicationId, which pre-derivation callers could always pass —
|
|
46
|
+
// degrades to "not shipped" with a warning.
|
|
47
|
+
const agentTokenParameterName = config.agentTokenSsmParameterName ??
|
|
48
|
+
this.deriveAgentSecretPath(config.applicationId, AGENT_TOKEN_SECRET_NAME);
|
|
49
|
+
const derivableApplicationId = config.applicationId !== undefined &&
|
|
50
|
+
config.applicationId !== "" &&
|
|
51
|
+
SSM_COMPONENT_PATTERN.test(config.applicationId)
|
|
52
|
+
? config.applicationId
|
|
53
|
+
: undefined;
|
|
54
|
+
const fjallApiKeyParameterName = config.fjallApiKeySsmParameterName ??
|
|
55
|
+
(derivableApplicationId !== undefined
|
|
56
|
+
? this.deriveAgentSecretPath(derivableApplicationId, FJALL_API_KEY_SECRET_NAME)
|
|
57
|
+
: undefined);
|
|
58
|
+
const pipelineSecretsPrefix = derivableApplicationId !== undefined
|
|
59
|
+
? buildParameterPath({
|
|
60
|
+
app: derivableApplicationId,
|
|
61
|
+
cluster: BUILDKITE_SECRETS_CLUSTER
|
|
62
|
+
})
|
|
63
|
+
: undefined;
|
|
64
|
+
if (derivableApplicationId === undefined &&
|
|
65
|
+
config.applicationId !== undefined &&
|
|
66
|
+
config.applicationId !== "") {
|
|
67
|
+
Annotations.of(this).addWarningV2("fjall:buildkite:fjall-api-key-hook-skipped", `applicationId '${config.applicationId}' is not a valid SSM path ` +
|
|
68
|
+
`component, so namespace-derived agent secrets are disabled: ` +
|
|
69
|
+
`per-pipeline job secrets will not load, and the FJALL_API_KEY env ` +
|
|
70
|
+
`hook is not shipped unless fjallApiKeySsmParameterName is set ` +
|
|
71
|
+
`explicitly. Rename the app or set the parameter-name props.`);
|
|
72
|
+
}
|
|
41
73
|
const stack = Stack.of(this);
|
|
42
74
|
if (Token.isUnresolved(stack.region)) {
|
|
43
75
|
throw new Error("Buildkite requires a concrete env region at synth (the pinned AMI " +
|
|
@@ -61,17 +93,27 @@ export class Buildkite extends Construct {
|
|
|
61
93
|
// and accidental overwrite; the artifact bucket is scratch output and
|
|
62
94
|
// keeps the wrapper's env-aware default (DESTROY + pre-empty off prod).
|
|
63
95
|
const managedSecretsBucket = new S3Bucket(this, `${id}ManagedSecretsBucket`, { versioned: true, removalPolicy: RemovalPolicy.RETAIN });
|
|
64
|
-
if (
|
|
65
|
-
|
|
96
|
+
if (fjallApiKeyParameterName !== undefined ||
|
|
97
|
+
pipelineSecretsPrefix !== undefined) {
|
|
98
|
+
this.addAgentSecretsEnvHook(id, managedSecretsBucket, {
|
|
99
|
+
...(fjallApiKeyParameterName !== undefined && {
|
|
100
|
+
fjallApiKeyParameterName
|
|
101
|
+
}),
|
|
102
|
+
...(pipelineSecretsPrefix !== undefined && { pipelineSecretsPrefix }),
|
|
103
|
+
region
|
|
104
|
+
});
|
|
66
105
|
}
|
|
67
106
|
const parameterArn = (name) => `arn:${stack.partition}:ssm:${region}:${stack.account}:parameter${name}`;
|
|
68
107
|
const agentRole = buildAgentRole(this, `${id}AgentRole`, {
|
|
69
|
-
agentTokenParameterArn: parameterArn(
|
|
108
|
+
agentTokenParameterArn: parameterArn(agentTokenParameterName),
|
|
70
109
|
...(config.agentTokenKmsKeyArn !== undefined && {
|
|
71
110
|
agentTokenKmsKeyArn: config.agentTokenKmsKeyArn
|
|
72
111
|
}),
|
|
73
|
-
...(
|
|
74
|
-
fjallApiKeyParameterArn: parameterArn(
|
|
112
|
+
...(fjallApiKeyParameterName !== undefined && {
|
|
113
|
+
fjallApiKeyParameterArn: parameterArn(fjallApiKeyParameterName)
|
|
114
|
+
}),
|
|
115
|
+
...(pipelineSecretsPrefix !== undefined && {
|
|
116
|
+
pipelineSecretsPrefixArn: parameterArn(pipelineSecretsPrefix)
|
|
75
117
|
}),
|
|
76
118
|
secretsBucketArn: managedSecretsBucket.bucketArn,
|
|
77
119
|
artifactBucketArn: artifactBucket.bucketArn,
|
|
@@ -82,7 +124,8 @@ export class Buildkite extends Construct {
|
|
|
82
124
|
stackName: stack.stackName,
|
|
83
125
|
region,
|
|
84
126
|
secretsBucketName: managedSecretsBucket.bucketName,
|
|
85
|
-
artifactBucketName: artifactBucket.bucketName
|
|
127
|
+
artifactBucketName: artifactBucket.bucketName,
|
|
128
|
+
agentTokenParameterName
|
|
86
129
|
});
|
|
87
130
|
// Deliberate deviation from upstream's InstanceScaleInProtection
|
|
88
131
|
// (design § D7): the wrapper's `newInstancesProtectedFromScaleIn: false`
|
|
@@ -146,7 +189,7 @@ export class Buildkite extends Construct {
|
|
|
146
189
|
applicationId: resolveScalerSarApplicationArn(architecture),
|
|
147
190
|
semanticVersion: BUILDKITE_STACK_PINS.scalerVersion,
|
|
148
191
|
parameters: {
|
|
149
|
-
BuildkiteAgentTokenParameter:
|
|
192
|
+
BuildkiteAgentTokenParameter: agentTokenParameterName,
|
|
150
193
|
...(config.agentTokenKmsKeyArn !== undefined && {
|
|
151
194
|
BuildkiteAgentTokenParameterStoreKMSKey: config.agentTokenKmsKeyArn
|
|
152
195
|
}),
|
|
@@ -201,18 +244,43 @@ export class Buildkite extends Construct {
|
|
|
201
244
|
value: artifactBucket.bucketName
|
|
202
245
|
});
|
|
203
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Derive an agent-cluster secret path from the app namespace — the same
|
|
249
|
+
* `buildParameterPath` derivation `fjall secrets` and the ECS pattern use,
|
|
250
|
+
* so derived parameters stay manageable by the shared tooling. Explicit
|
|
251
|
+
* props bypass this entirely; it throws only when derivation is the last
|
|
252
|
+
* resort and no valid `applicationId` exists to derive from.
|
|
253
|
+
*/
|
|
254
|
+
deriveAgentSecretPath(applicationId, secretName) {
|
|
255
|
+
if (applicationId === undefined || applicationId === "") {
|
|
256
|
+
throw new Error(`Buildkite: no explicit SSM parameter name was set for '${secretName}' ` +
|
|
257
|
+
`and no applicationId is available to derive ` +
|
|
258
|
+
`'/<applicationId>/${BUILDKITE_SECRETS_CLUSTER}/${secretName}' from. ` +
|
|
259
|
+
`Construct via App.addBuildkite (which injects applicationId) or set ` +
|
|
260
|
+
`the parameter-name prop explicitly.`);
|
|
261
|
+
}
|
|
262
|
+
if (!SSM_COMPONENT_PATTERN.test(applicationId)) {
|
|
263
|
+
throw new Error(`Buildkite: cannot derive the '${secretName}' SSM parameter path — ` +
|
|
264
|
+
`applicationId '${applicationId}' is not a valid SSM path component ` +
|
|
265
|
+
`(must start with a letter; letters, numbers, periods, hyphens, ` +
|
|
266
|
+
`underscores only). Rename the app or set the parameter-name prop ` +
|
|
267
|
+
`explicitly.`);
|
|
268
|
+
}
|
|
269
|
+
return buildParameterPath({ app: applicationId, cluster: BUILDKITE_SECRETS_CLUSTER }, secretName);
|
|
270
|
+
}
|
|
204
271
|
/**
|
|
205
272
|
* Ship the per-job `env` hook into the managed secrets bucket. The
|
|
206
273
|
* s3-secrets-hooks plugin sources this file at the start of EVERY job, so
|
|
207
|
-
* FJALL_API_KEY
|
|
208
|
-
* the next job with no instance
|
|
209
|
-
*
|
|
210
|
-
*
|
|
274
|
+
* FJALL_API_KEY and the per-pipeline job secrets are read fresh from SSM
|
|
275
|
+
* per job — rotation takes effect on the next job with no instance
|
|
276
|
+
* replacement (design § D4(iii)). The script is configuration, not a
|
|
277
|
+
* secret: shipping it through a CDK asset is fine; secret VALUES only ever
|
|
278
|
+
* move SSM → instance at job runtime.
|
|
211
279
|
*/
|
|
212
|
-
|
|
280
|
+
addAgentSecretsEnvHook(id, secretsBucket, options) {
|
|
213
281
|
new BucketDeployment(this, `${id}EnvHookDeployment`, {
|
|
214
282
|
sources: [
|
|
215
|
-
Source.data(FJALL_ENV_HOOK_OBJECT_KEY,
|
|
283
|
+
Source.data(FJALL_ENV_HOOK_OBJECT_KEY, buildAgentSecretsEnvHookScript(options))
|
|
216
284
|
],
|
|
217
285
|
destinationBucket: secretsBucket,
|
|
218
286
|
// The secrets bucket also holds out-of-band objects the deployment
|
|
@@ -230,17 +298,57 @@ export class Buildkite extends Construct {
|
|
|
230
298
|
*/
|
|
231
299
|
export const FJALL_ENV_HOOK_OBJECT_KEY = "env";
|
|
232
300
|
/**
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
301
|
+
* Cluster segment of derived agent secret paths
|
|
302
|
+
* (`/<applicationId>/agents/<name>`) — the namespace the live estate already
|
|
303
|
+
* uses for its explicitly-provisioned parameters.
|
|
304
|
+
*/
|
|
305
|
+
export const BUILDKITE_SECRETS_CLUSTER = "agents";
|
|
306
|
+
/** Derived secret name for the cluster-scoped Buildkite agent token. */
|
|
307
|
+
export const AGENT_TOKEN_SECRET_NAME = "agent-token";
|
|
308
|
+
/** Derived secret name for the FJALL_API_KEY env-hook parameter. */
|
|
309
|
+
export const FJALL_API_KEY_SECRET_NAME = "fjall-api-key";
|
|
310
|
+
/**
|
|
311
|
+
* The per-job env hook's script content (design § D4(iii) + Phase B):
|
|
312
|
+
* FJALL_API_KEY and the per-pipeline job secrets are read fresh from SSM per
|
|
313
|
+
* job, so rotation takes effect on the next job with no instance replacement.
|
|
314
|
+
* Pure — the unit tests byte-pin both emitted variants and `sh -n` them.
|
|
315
|
+
*
|
|
316
|
+
* The script is eval'd by the s3secrets plugin under bash
|
|
317
|
+
* `set -Eeuo pipefail` + an ERR trap that exits 53 (fails the job) +
|
|
318
|
+
* `set -o allexport` (every assignment auto-exports) + `nounset` — pinned in
|
|
319
|
+
* `lib/__tests__/fixtures/buildkite/upstream-hooks-v6.68.1/`. Hence: every
|
|
320
|
+
* fallible command is guarded, every env read uses `${VAR:-}`, every temp
|
|
321
|
+
* variable is unset after use, and the script never exits. `private_ssh_key`
|
|
322
|
+
* loads into an ephemeral ssh-agent (stdin → agent memory, never disk or
|
|
323
|
+
* argv); the upstream pre-exit hook kills whatever `SSH_AGENT_PID` is
|
|
324
|
+
* exported, so our agent's lifecycle is owned upstream. Every fetched value
|
|
325
|
+
* is registered with the agent redactor (stdin form) before use — the
|
|
326
|
+
* upstream helper cannot see values we fetch at eval time.
|
|
236
327
|
*/
|
|
237
|
-
export function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
""
|
|
242
|
-
|
|
328
|
+
export function buildAgentSecretsEnvHookScript(options) {
|
|
329
|
+
const { fjallApiKeyParameterName, pipelineSecretsPrefix, region } = options;
|
|
330
|
+
const lines = ["set +x", "set +v"];
|
|
331
|
+
if (fjallApiKeyParameterName !== undefined) {
|
|
332
|
+
lines.push(`FJALL_API_KEY="$(aws ssm get-parameter --name '${fjallApiKeyParameterName}' --with-decryption --query Parameter.Value --output text --region '${region}' 2>/dev/null)" || true`, 'if [ -n "${FJALL_API_KEY:-}" ]; then', ` printf '%s' "$FJALL_API_KEY" | buildkite-agent redactor add >/dev/null 2>&1 || true`, " export FJALL_API_KEY", "else", " unset FJALL_API_KEY", ` echo 'fjall-secrets: could not fetch ${fjallApiKeyParameterName}' >&2`, "fi");
|
|
333
|
+
}
|
|
334
|
+
if (pipelineSecretsPrefix !== undefined) {
|
|
335
|
+
lines.push('if [ -n "${BUILDKITE_PIPELINE_SLUG:-}" ]; then', ` _fjall_names="$(aws ssm get-parameters-by-path --path "${pipelineSecretsPrefix}/\${BUILDKITE_PIPELINE_SLUG:-}" --query 'Parameters[].Name' --output text --region '${region}' 2>/dev/null)" || true`, " for _fjall_param in ${_fjall_names:-}; do", ' _fjall_name="${_fjall_param##*/}"', ` _fjall_value="$(aws ssm get-parameter --name "$_fjall_param" --with-decryption --query Parameter.Value --output text --region '${region}' 2>/dev/null)" || true`, ' if [ -z "${_fjall_value:-}" ]; then', ' echo "fjall-secrets: could not fetch $_fjall_param" >&2', " unset _fjall_value", " continue", " fi", ` printf '%s' "$_fjall_value" | buildkite-agent redactor add >/dev/null 2>&1 || true`, ` if [ "$_fjall_name" = 'private_ssh_key' ]; then`, ` _fjall_agent_out="$(ssh-agent -s 2>/dev/null)" || true`, ' if [ -n "${_fjall_agent_out:-}" ]; then', ' eval "$_fjall_agent_out" >/dev/null 2>&1 || true', ` printf '%s\\n' "$_fjall_value" | ssh-add - >/dev/null 2>&1 || echo 'fjall-secrets: ssh-add failed for private_ssh_key' >&2`, " else", ` echo 'fjall-secrets: ssh-agent unavailable - private_ssh_key not loaded' >&2`, " fi", " unset _fjall_agent_out", ` elif printf '%s' "$_fjall_name" | grep -Eq '${JOB_ENV_NAME_ALLOW_REGEX}' && ! printf '%s' "$_fjall_name" | grep -Eq '${JOB_ENV_NAME_DENY_REGEX}'; then`, ` export "$_fjall_name=$_fjall_value" 2>/dev/null || echo "fjall-secrets: could not export $_fjall_name" >&2`, " else", ' echo "fjall-secrets: skipping non-conforming secret name $_fjall_name" >&2', " fi", " unset _fjall_value", " done", " unset _fjall_names", " unset _fjall_param", " unset _fjall_name", "fi");
|
|
336
|
+
}
|
|
337
|
+
lines.push("");
|
|
338
|
+
return lines.join("\n");
|
|
243
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Job-env eligibility for per-pipeline secret names: the allow shape is a
|
|
342
|
+
* portable shell identifier; the deny list removes the bash-readonly names
|
|
343
|
+
* (`export UID=…` fails "readonly variable" — under the upstream ERR trap
|
|
344
|
+
* that would exit-53 EVERY job on the pipeline until the parameter is
|
|
345
|
+
* deleted), the ssh-agent lifecycle vars the D-B3 flow owns, the
|
|
346
|
+
* loader-injection vector vars, and the `BUILDKITE_`/`AWS_` prefixes.
|
|
347
|
+
* `private_ssh_key` is handled before this filter; denied/non-conforming
|
|
348
|
+
* names are skipped with an identifier-only warning.
|
|
349
|
+
*/
|
|
350
|
+
const JOB_ENV_NAME_ALLOW_REGEX = "^[A-Za-z_][A-Za-z0-9_]*$";
|
|
351
|
+
const JOB_ENV_NAME_DENY_REGEX = "^(PATH|HOME|IFS|LD_PRELOAD|LD_LIBRARY_PATH|BASH_ENV|SSH_AUTH_SOCK|SSH_AGENT_PID|UID|EUID|PPID|SHELLOPTS|BASHOPTS|BASH_VERSINFO)$|^(BUILDKITE_|AWS_)";
|
|
244
352
|
function deriveCpuArchitecture(instanceTypeIdentifier) {
|
|
245
353
|
const architecture = new InstanceType(instanceTypeIdentifier).architecture;
|
|
246
354
|
return architecture === InstanceArchitecture.ARM_64 ? "arm64" : "amd64";
|
|
@@ -11,11 +11,18 @@ import { Role } from "../../../resources/aws/iam/index.js";
|
|
|
11
11
|
* subset of this list, so an upstream bump (or a future edit) that grows
|
|
12
12
|
* permissions fails loudly instead of shipping silently.
|
|
13
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"];
|
|
14
|
+
export declare const BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST: readonly ["ssm:GetParameter", "ssm:GetParametersByPath", "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
15
|
export interface BuildkiteAgentRoleParams {
|
|
16
16
|
readonly agentTokenParameterArn: string;
|
|
17
17
|
readonly agentTokenKmsKeyArn?: string;
|
|
18
18
|
readonly fjallApiKeyParameterArn?: string;
|
|
19
|
+
/**
|
|
20
|
+
* `arn:…:parameter/<app>/agents` — per-pipeline job-secrets subtree root;
|
|
21
|
+
* grants list + read on `<root>/*`. Jobs can read the whole subtree
|
|
22
|
+
* (identical flatness to the whole-bucket S3 read this replaces);
|
|
23
|
+
* isolation stays per-fleet.
|
|
24
|
+
*/
|
|
25
|
+
readonly pipelineSecretsPrefixArn?: string;
|
|
19
26
|
readonly secretsBucketArn: string;
|
|
20
27
|
readonly artifactBucketArn: string;
|
|
21
28
|
/** Own-stack ARN — `cloudformation:DescribeStackResource` scope. */
|
|
@@ -13,6 +13,9 @@ import { Role } from "../../../resources/aws/iam/index.js";
|
|
|
13
13
|
*/
|
|
14
14
|
export const BUILDKITE_AGENT_IAM_ACTION_ALLOWLIST = [
|
|
15
15
|
"ssm:GetParameter",
|
|
16
|
+
// Read-only, scoped to the /<app>/agents/* subtree (per-pipeline job
|
|
17
|
+
// secrets) — never account-wide.
|
|
18
|
+
"ssm:GetParametersByPath",
|
|
16
19
|
"kms:Decrypt",
|
|
17
20
|
"s3:GetObject",
|
|
18
21
|
"s3:ListBucket",
|
|
@@ -77,6 +80,14 @@ function buildAgentPolicyDocuments(params) {
|
|
|
77
80
|
: [])
|
|
78
81
|
]
|
|
79
82
|
}),
|
|
83
|
+
...(params.pipelineSecretsPrefixArn !== undefined
|
|
84
|
+
? [
|
|
85
|
+
new PolicyStatement({
|
|
86
|
+
actions: ["ssm:GetParameter", "ssm:GetParametersByPath"],
|
|
87
|
+
resources: [`${params.pipelineSecretsPrefixArn}/*`]
|
|
88
|
+
})
|
|
89
|
+
]
|
|
90
|
+
: []),
|
|
80
91
|
...(params.agentTokenKmsKeyArn !== undefined
|
|
81
92
|
? [
|
|
82
93
|
new PolicyStatement({
|
|
@@ -11,6 +11,13 @@ export interface BuildkiteUserDataContext {
|
|
|
11
11
|
readonly region: string;
|
|
12
12
|
readonly secretsBucketName: string;
|
|
13
13
|
readonly artifactBucketName: string;
|
|
14
|
+
/**
|
|
15
|
+
* RESOLVED agent-token parameter path (explicit prop or namespace-derived)
|
|
16
|
+
* — never `props.agentTokenSsmParameterName`, which is optional since the
|
|
17
|
+
* derivation landed and would render the literal string "undefined" into
|
|
18
|
+
* BUILDKITE_AGENT_TOKEN_PATH through the template literal.
|
|
19
|
+
*/
|
|
20
|
+
readonly agentTokenParameterName: string;
|
|
14
21
|
}
|
|
15
22
|
/**
|
|
16
23
|
* The pinned AMI's boot scripts run under `set -Eeuo pipefail` and
|
|
@@ -42,7 +49,7 @@ export declare function buildBuildkiteDockerCommands(props: BuildkiteProps): str
|
|
|
42
49
|
* `bk-install-elastic-stack.sh`, mirroring the upstream block key-for-key in
|
|
43
50
|
* upstream order (so a fixture-bump diff reads line-by-line). Keys with no
|
|
44
51
|
* schema knob carry the upstream v6.68.1 parameter default verbatim, with
|
|
45
|
-
*
|
|
52
|
+
* five deliberate deviations:
|
|
46
53
|
*
|
|
47
54
|
* - `BUILDKITE_S3_ACL`: our artifact bucket is BucketOwnerEnforced (ACLs
|
|
48
55
|
* disabled), where S3 rejects every canned ACL EXCEPT
|
|
@@ -54,6 +61,11 @@ export declare function buildBuildkiteDockerCommands(props: BuildkiteProps): str
|
|
|
54
61
|
* is out of scope for the Phase-1 deploy fleet; a future signing knob must
|
|
55
62
|
* land WITH the kms:Sign/Verify/GetPublicKey grants it needs (the prior
|
|
56
63
|
* knob shipped without them — a dead toggle that broke agents when set).
|
|
64
|
+
* - `BUILDKITE_SECRETS_PLUGIN_SKIP_SSH_KEY_NOT_FOUND_WARNING`: hard-pinned
|
|
65
|
+
* `true` (upstream default `false`). Since the Phase-B migration the git
|
|
66
|
+
* deploy key lives in SSM and is loaded by our env hook — the plugin's S3
|
|
67
|
+
* `private_ssh_key` object is intentionally absent, so its per-job
|
|
68
|
+
* not-found warning is pure noise.
|
|
57
69
|
* - `ENABLE_EC2_LOG_RETENTION_POLICY`: hard-pinned `true` (upstream default
|
|
58
70
|
* `false`, "preserve all logs"). Fjall's log-group lifecycle posture is
|
|
59
71
|
* bounded retention everywhere: each booting instance applies
|
|
@@ -149,7 +149,7 @@ export function buildBuildkiteDockerCommands(props) {
|
|
|
149
149
|
* `bk-install-elastic-stack.sh`, mirroring the upstream block key-for-key in
|
|
150
150
|
* upstream order (so a fixture-bump diff reads line-by-line). Keys with no
|
|
151
151
|
* schema knob carry the upstream v6.68.1 parameter default verbatim, with
|
|
152
|
-
*
|
|
152
|
+
* five deliberate deviations:
|
|
153
153
|
*
|
|
154
154
|
* - `BUILDKITE_S3_ACL`: our artifact bucket is BucketOwnerEnforced (ACLs
|
|
155
155
|
* disabled), where S3 rejects every canned ACL EXCEPT
|
|
@@ -161,6 +161,11 @@ export function buildBuildkiteDockerCommands(props) {
|
|
|
161
161
|
* is out of scope for the Phase-1 deploy fleet; a future signing knob must
|
|
162
162
|
* land WITH the kms:Sign/Verify/GetPublicKey grants it needs (the prior
|
|
163
163
|
* knob shipped without them — a dead toggle that broke agents when set).
|
|
164
|
+
* - `BUILDKITE_SECRETS_PLUGIN_SKIP_SSH_KEY_NOT_FOUND_WARNING`: hard-pinned
|
|
165
|
+
* `true` (upstream default `false`). Since the Phase-B migration the git
|
|
166
|
+
* deploy key lives in SSM and is loaded by our env hook — the plugin's S3
|
|
167
|
+
* `private_ssh_key` object is intentionally absent, so its per-job
|
|
168
|
+
* not-found warning is pure noise.
|
|
164
169
|
* - `ENABLE_EC2_LOG_RETENTION_POLICY`: hard-pinned `true` (upstream default
|
|
165
170
|
* `false`, "preserve all logs"). Fjall's log-group lifecycle posture is
|
|
166
171
|
* bounded retention everywhere: each booting instance applies
|
|
@@ -181,11 +186,11 @@ export function buildBuildkiteInstallCommands(props, context) {
|
|
|
181
186
|
`BUILDKITE_SCALE_IN_IDLE_PERIOD='${props.scaleInIdlePeriodSeconds}' \\`,
|
|
182
187
|
`BUILDKITE_SECRETS_BUCKET='${context.secretsBucketName}' \\`,
|
|
183
188
|
`BUILDKITE_SECRETS_BUCKET_REGION='${context.region}' \\`,
|
|
184
|
-
"BUILDKITE_SECRETS_PLUGIN_SKIP_SSH_KEY_NOT_FOUND_WARNING='
|
|
189
|
+
"BUILDKITE_SECRETS_PLUGIN_SKIP_SSH_KEY_NOT_FOUND_WARNING='true' \\",
|
|
185
190
|
`BUILDKITE_ARTIFACTS_BUCKET='${context.artifactBucketName}' \\`,
|
|
186
191
|
`BUILDKITE_S3_DEFAULT_REGION='${context.region}' \\`,
|
|
187
192
|
"BUILDKITE_S3_ACL='bucket-owner-full-control' \\",
|
|
188
|
-
`BUILDKITE_AGENT_TOKEN_PATH='${
|
|
193
|
+
`BUILDKITE_AGENT_TOKEN_PATH='${context.agentTokenParameterName}' \\`,
|
|
189
194
|
`BUILDKITE_AGENTS_PER_INSTANCE='${props.agentsPerInstance}' \\`,
|
|
190
195
|
"BUILDKITE_AGENT_ENDPOINT='https://agent-edge.buildkite.com/v3' \\",
|
|
191
196
|
`BUILDKITE_AGENT_TAGS='${props.buildkiteAgentTags}' \\`,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Construct instantiated via `App.addBuildkite()`; design:
|
|
5
5
|
* `aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`.
|
|
6
6
|
*/
|
|
7
|
-
export { Buildkite, FJALL_ENV_HOOK_OBJECT_KEY,
|
|
7
|
+
export { AGENT_TOKEN_SECRET_NAME, BUILDKITE_SECRETS_CLUSTER, Buildkite, FJALL_API_KEY_SECRET_NAME, FJALL_ENV_HOOK_OBJECT_KEY, buildAgentSecretsEnvHookScript, type AgentSecretsEnvHookOptions, type BuildkiteConstructProps } from "./buildkite/buildkite.js";
|
|
8
8
|
export { BUILDKITE_AMI_OWNER_ACCOUNT_ID, BUILDKITE_CPU_ARCHITECTURES, BUILDKITE_STACK_PINS, resolvePinnedAmiId, resolveScalerSarApplicationArn, type BuildkiteCpuArchitecture } from "./buildkite/pins.js";
|
|
9
9
|
export { BuildkitePropsSchema, validateBuildkiteProps, type BuildkiteProps, type BuildkitePropsInput } from "./buildkite/schema.js";
|
|
10
10
|
export { BuildkiteFactory, type BuildkiteFactoryFn } from "./buildkite/factory.js";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Construct instantiated via `App.addBuildkite()`; design:
|
|
5
5
|
* `aiDocs/designs/2026-07-18-buildkite-selfhosted-agents.md`.
|
|
6
6
|
*/
|
|
7
|
-
export { Buildkite, FJALL_ENV_HOOK_OBJECT_KEY,
|
|
7
|
+
export { AGENT_TOKEN_SECRET_NAME, BUILDKITE_SECRETS_CLUSTER, Buildkite, FJALL_API_KEY_SECRET_NAME, FJALL_ENV_HOOK_OBJECT_KEY, buildAgentSecretsEnvHookScript } from "./buildkite/buildkite.js";
|
|
8
8
|
export { BUILDKITE_AMI_OWNER_ACCOUNT_ID, BUILDKITE_CPU_ARCHITECTURES, BUILDKITE_STACK_PINS, resolvePinnedAmiId, resolveScalerSarApplicationArn } from "./buildkite/pins.js";
|
|
9
9
|
export { BuildkitePropsSchema, validateBuildkiteProps } from "./buildkite/schema.js";
|
|
10
10
|
export { BuildkiteFactory } from "./buildkite/factory.js";
|
|
@@ -69,6 +69,12 @@ export interface AuroraDatabaseProps extends BaseDatabaseProps {
|
|
|
69
69
|
clusterIdentifier?: string;
|
|
70
70
|
/** Physical name of the generated master-credentials secret. */
|
|
71
71
|
credentialsSecretName?: string;
|
|
72
|
+
/**
|
|
73
|
+
* KMS alias for the CMK encrypting the master-credentials secret. Must be
|
|
74
|
+
* app-scoped when the same `databaseName` deploys once per app in a shared
|
|
75
|
+
* account (aliases are account+region-unique).
|
|
76
|
+
*/
|
|
77
|
+
credentialsKmsAliasName?: string;
|
|
72
78
|
/** Serverless-v2 floor in ACU. 0 enables scale-to-zero (auto-pause). */
|
|
73
79
|
serverlessV2MinCapacity?: number;
|
|
74
80
|
/** Serverless-v2 ceiling in ACU. */
|
|
@@ -16,7 +16,7 @@ import { Duration } from "aws-cdk-lib";
|
|
|
16
16
|
export { isAwsManagedKey, isCMKRequested, AWS_MANAGED, USE_CMK } from "../../utils/databaseTypes.js";
|
|
17
17
|
export const DATABASE_ENGINE_CONFIG = Object.freeze({
|
|
18
18
|
postgresql: {
|
|
19
|
-
aurora: { version: "16.
|
|
19
|
+
aurora: { version: "16.10", majorVersion: "16" },
|
|
20
20
|
instance: { version: "17.5", majorVersion: "17" },
|
|
21
21
|
defaultUsername: "postgres",
|
|
22
22
|
sslParameters: { "rds.force_ssl": "1" }
|
|
@@ -372,6 +372,7 @@ export class RelationalDatabase extends Construct {
|
|
|
372
372
|
engineConfig,
|
|
373
373
|
clusterIdentifier: props.clusterIdentifier,
|
|
374
374
|
credentialsSecretName: props.credentialsSecretName,
|
|
375
|
+
credentialsKmsAliasName: props.credentialsKmsAliasName,
|
|
375
376
|
serverlessV2MinCapacity: props.serverlessV2MinCapacity,
|
|
376
377
|
serverlessV2MaxCapacity: props.serverlessV2MaxCapacity,
|
|
377
378
|
serverlessV2AutoPauseDuration: props.serverlessV2AutoPauseDuration,
|
|
@@ -10,8 +10,12 @@ export interface DelegatedDomainPatternResult {
|
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Composition for `registrar: "external-delegated"` — the delegated-child
|
|
13
|
-
* topology. Creates a hosted zone for `{delegatedSubdomain}.{zoneName}`
|
|
14
|
-
*
|
|
13
|
+
* topology. Creates a hosted zone for `{delegatedSubdomain}.{zoneName}` —
|
|
14
|
+
* or ADOPTS an existing one when `hostedZoneId` + `adoptedNameServers` are
|
|
15
|
+
* set (both-or-neither, validated): the zone is imported instead of created
|
|
16
|
+
* and the delegation UPSERT carries the adopted NS literals, since an
|
|
17
|
+
* imported zone exposes no `hostedZoneNameServers` attribute. When
|
|
18
|
+
* `parentDelegationRoleArn` is present, the child NS are UPSERTed into the
|
|
15
19
|
* parent zone via `CrossAccountZoneDelegationRecord` (D8 child-writes
|
|
16
20
|
* delegation; same shape as devSubstrate Case 1). The role ARN MUST be a
|
|
17
21
|
* literal — validated in `validateDomainProps` (VD2-a). When absent, the
|