@fjall/components-infrastructure 3.4.1 → 3.5.2
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/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/devSubstrate.js +15 -5
- 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/secrets/secret.d.ts +6 -0
- package/dist/lib/resources/aws/secrets/secret.js +1 -1
- package/package.json +4 -4
|
@@ -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,
|
|
@@ -161,6 +161,8 @@ export class DevSubstrate extends Construct {
|
|
|
161
161
|
databaseName: DEV_SUBSTRATE_DATABASE_NAME,
|
|
162
162
|
clusterIdentifier: devFenceName(props.appKebab),
|
|
163
163
|
credentialsSecretName: `${devFenceName(props.appKebab)}-master`,
|
|
164
|
+
// Default alias derives from the fixed databaseName — collides account-wide.
|
|
165
|
+
credentialsKmsAliasName: `cmk/${devFenceName(props.appKebab)}-master`,
|
|
164
166
|
engineVersion: props.engineVersion ?? DEV_AURORA_DEFAULTS.ENGINE_VERSION,
|
|
165
167
|
serverlessV2MinCapacity: DEV_AURORA_DEFAULTS.SERVERLESS_V2_MIN_CAPACITY,
|
|
166
168
|
serverlessV2MaxCapacity: DEV_AURORA_DEFAULTS.SERVERLESS_V2_MAX_CAPACITY,
|
|
@@ -205,8 +207,14 @@ export class DevSubstrate extends Construct {
|
|
|
205
207
|
// createBaseExecutionRole DELIBERATELY omits logs (auto-granted only on the
|
|
206
208
|
// ECS-pattern path via AwsLogDriver.bind()). Slot task-defs reference this
|
|
207
209
|
// shared role by ARN, so that auto-grant never fires — add the log-write grant
|
|
208
|
-
// explicitly.
|
|
209
|
-
|
|
210
|
+
// explicitly. Path + name pin the role inside the /fjall/dev/ fence: the dev
|
|
211
|
+
// deploy role's only iam:PassRole grant is on role/fjall/dev/*, so the CDK
|
|
212
|
+
// defaults (path "/", CFN-generated name) would AccessDeny slot task-def
|
|
213
|
+
// registration.
|
|
214
|
+
const slotExecutionRole = createBaseExecutionRole(this, DEV_SUBSTRATE_SLOT_EXEC_ROLE_ID, {
|
|
215
|
+
path: "/fjall/dev/",
|
|
216
|
+
roleName: `${devFenceName(props.appKebab)}-slot-exec`
|
|
217
|
+
});
|
|
210
218
|
const { partition, region, account } = Stack.of(this);
|
|
211
219
|
slotExecutionRole.addToPolicy(new PolicyStatement({
|
|
212
220
|
effect: Effect.ALLOW,
|
|
@@ -376,9 +384,11 @@ export class DevSubstrate extends Construct {
|
|
|
376
384
|
* the hand-created G2 slot service consumes these to place tasks, register a
|
|
377
385
|
* host-header rule, inject DB creds and point a sleeping slot at the waker.
|
|
378
386
|
*
|
|
379
|
-
* No KMS-alias export:
|
|
380
|
-
*
|
|
381
|
-
*
|
|
387
|
+
* No KMS-alias export: cluster storage and SSM SecureStrings ride AWS-managed
|
|
388
|
+
* default keys, and while the master-credentials secret IS CMK-encrypted (the
|
|
389
|
+
* Secret wrapper mints `cmk/fjall-dev-<app>-master`), no consumer addresses
|
|
390
|
+
* that key by alias — the provisioner's and waker's kms:Decrypt are
|
|
391
|
+
* ViaService-scoped (Secrets Manager / SSM), so there is no alias to export.
|
|
382
392
|
*
|
|
383
393
|
* The Aurora slot-SG ingress (A2.5) makes the Database stack depend on the
|
|
384
394
|
* Compute stack. So the two DB exports are scoped under `databaseScope` (the
|
|
@@ -16,9 +16,16 @@ import type { EcsServiceProps } from "./ecsTypes.js";
|
|
|
16
16
|
* SHARED or IMPORTED role those auto-grants cannot reach — e.g. the dev
|
|
17
17
|
* substrate's slot exec role, referenced by slot task-defs via ARN — MUST add
|
|
18
18
|
* the explicit `logs` / `secretsmanager` statements itself.
|
|
19
|
+
*
|
|
20
|
+
* `path`/`roleName` pin the IAM path and physical name — needed when an IAM
|
|
21
|
+
* grant fences on the role's path (the dev-role `iam:PassRole` grant on
|
|
22
|
+
* `role/fjall/dev/*`), since the CDK default is path `/` + a CFN-generated
|
|
23
|
+
* name no path- or prefix-scoped grant can match.
|
|
19
24
|
*/
|
|
20
25
|
export declare function createBaseExecutionRole(scope: Construct, id: string, opts?: {
|
|
21
26
|
ssmSecretsPath?: string;
|
|
27
|
+
path?: string;
|
|
28
|
+
roleName?: string;
|
|
22
29
|
}): Role;
|
|
23
30
|
/**
|
|
24
31
|
* Creates the execution role for ECS infrastructure operations.
|
|
@@ -15,10 +15,17 @@ import { deriveSsmSecretsPath } from "./ecsTaskDefinition.js";
|
|
|
15
15
|
* SHARED or IMPORTED role those auto-grants cannot reach — e.g. the dev
|
|
16
16
|
* substrate's slot exec role, referenced by slot task-defs via ARN — MUST add
|
|
17
17
|
* the explicit `logs` / `secretsmanager` statements itself.
|
|
18
|
+
*
|
|
19
|
+
* `path`/`roleName` pin the IAM path and physical name — needed when an IAM
|
|
20
|
+
* grant fences on the role's path (the dev-role `iam:PassRole` grant on
|
|
21
|
+
* `role/fjall/dev/*`), since the CDK default is path `/` + a CFN-generated
|
|
22
|
+
* name no path- or prefix-scoped grant can match.
|
|
18
23
|
*/
|
|
19
24
|
export function createBaseExecutionRole(scope, id, opts = {}) {
|
|
20
25
|
const executionRole = new Role(scope, id, {
|
|
21
|
-
assumedBy: new ServicePrincipal("ecs-tasks.amazonaws.com")
|
|
26
|
+
assumedBy: new ServicePrincipal("ecs-tasks.amazonaws.com"),
|
|
27
|
+
path: opts.path,
|
|
28
|
+
roleName: opts.roleName
|
|
22
29
|
});
|
|
23
30
|
// GetAuthorizationToken is an account-level API that requires resources: ["*"].
|
|
24
31
|
// The image-pull actions also use "*" because ecrRepository can be a string URI
|
|
@@ -20,6 +20,13 @@ interface RdsProps {
|
|
|
20
20
|
clusterIdentifier?: string;
|
|
21
21
|
/** Physical name of the generated master-credentials secret. */
|
|
22
22
|
credentialsSecretName?: string;
|
|
23
|
+
/**
|
|
24
|
+
* KMS alias for the CMK encrypting the master-credentials secret. Aliases are
|
|
25
|
+
* account+region-unique, so a caller with a fixed `databaseName` deployed once
|
|
26
|
+
* per app (e.g. the dev substrate) must pass an app-scoped alias or the second
|
|
27
|
+
* app's deploy fails on the alias collision.
|
|
28
|
+
*/
|
|
29
|
+
credentialsKmsAliasName?: string;
|
|
23
30
|
/** Serverless-v2 floor in ACU. 0 enables scale-to-zero (auto-pause). */
|
|
24
31
|
serverlessV2MinCapacity?: number;
|
|
25
32
|
/** Serverless-v2 ceiling in ACU. */
|
|
@@ -100,6 +100,7 @@ export class RdsAurora extends Construct {
|
|
|
100
100
|
this.databaseCredentials = new Secret(this, `${this.databaseNameValue}Credentials`, {
|
|
101
101
|
secretName: props.credentialsSecretName ??
|
|
102
102
|
ResourceNaming.credentialsSecretName(id),
|
|
103
|
+
aliasName: props.credentialsKmsAliasName,
|
|
103
104
|
generateSecretString: {
|
|
104
105
|
secretStringTemplate: JSON.stringify({ username }),
|
|
105
106
|
excludePunctuation: true,
|
|
@@ -151,7 +152,7 @@ export class RdsAurora extends Construct {
|
|
|
151
152
|
const readers = this.buildReaders(props, piEnabled, performanceInsightsKey, performanceInsightsRetention);
|
|
152
153
|
const engine = props.engine ||
|
|
153
154
|
DatabaseClusterEngine.auroraPostgres({
|
|
154
|
-
version: AuroraPostgresEngineVersion.of("16.
|
|
155
|
+
version: AuroraPostgresEngineVersion.of("16.10", "16")
|
|
155
156
|
});
|
|
156
157
|
const parameterGroup = new ParameterGroup(this, `${this.databaseNameValue}ParameterGroup`, {
|
|
157
158
|
engine,
|
|
@@ -16,15 +16,18 @@ export declare const RDS_DEFAULTS: Readonly<{
|
|
|
16
16
|
* (fast dev-envs Phase 3). `min 0` ACU scales the shared cluster to zero when
|
|
17
17
|
* idle; `enableDataApi` lets slot DDL run over HTTPS with no VPC connection, and
|
|
18
18
|
* is the only mode under which min-0 actually saves (a live pooled connection
|
|
19
|
-
* pins the cluster above 0 ACU). Engine 16.
|
|
20
|
-
* floor (see `MIN_AUTO_PAUSE_POSTGRES_VERSION`).
|
|
19
|
+
* pins the cluster above 0 ACU). Engine 16.10 satisfies the auto-pause-capable
|
|
20
|
+
* floor (see `MIN_AUTO_PAUSE_POSTGRES_VERSION`). AWS removes old minors from
|
|
21
|
+
* the creatable list over time (16.6 rejected with "Cannot find version" by
|
|
22
|
+
* 2026-07) — pin a mid-window minor, not the oldest available, and expect this
|
|
23
|
+
* to need a bump roughly yearly.
|
|
21
24
|
*/
|
|
22
25
|
export declare const DEV_AURORA_DEFAULTS: Readonly<{
|
|
23
26
|
readonly SERVERLESS_V2_MIN_CAPACITY: 0;
|
|
24
27
|
readonly SERVERLESS_V2_MAX_CAPACITY: 4;
|
|
25
28
|
readonly AUTO_PAUSE_SECONDS: 1200;
|
|
26
29
|
readonly ENABLE_DATA_API: true;
|
|
27
|
-
readonly ENGINE_VERSION: "16.
|
|
30
|
+
readonly ENGINE_VERSION: "16.10";
|
|
28
31
|
}>;
|
|
29
32
|
/**
|
|
30
33
|
* Bounds AWS enforces on `ServerlessV2ScalingConfiguration.SecondsUntilAutoPause`
|
|
@@ -16,15 +16,18 @@ export const RDS_DEFAULTS = Object.freeze({
|
|
|
16
16
|
* (fast dev-envs Phase 3). `min 0` ACU scales the shared cluster to zero when
|
|
17
17
|
* idle; `enableDataApi` lets slot DDL run over HTTPS with no VPC connection, and
|
|
18
18
|
* is the only mode under which min-0 actually saves (a live pooled connection
|
|
19
|
-
* pins the cluster above 0 ACU). Engine 16.
|
|
20
|
-
* floor (see `MIN_AUTO_PAUSE_POSTGRES_VERSION`).
|
|
19
|
+
* pins the cluster above 0 ACU). Engine 16.10 satisfies the auto-pause-capable
|
|
20
|
+
* floor (see `MIN_AUTO_PAUSE_POSTGRES_VERSION`). AWS removes old minors from
|
|
21
|
+
* the creatable list over time (16.6 rejected with "Cannot find version" by
|
|
22
|
+
* 2026-07) — pin a mid-window minor, not the oldest available, and expect this
|
|
23
|
+
* to need a bump roughly yearly.
|
|
21
24
|
*/
|
|
22
25
|
export const DEV_AURORA_DEFAULTS = Object.freeze({
|
|
23
26
|
SERVERLESS_V2_MIN_CAPACITY: 0,
|
|
24
27
|
SERVERLESS_V2_MAX_CAPACITY: 4,
|
|
25
28
|
AUTO_PAUSE_SECONDS: 1200,
|
|
26
29
|
ENABLE_DATA_API: true,
|
|
27
|
-
ENGINE_VERSION: "16.
|
|
30
|
+
ENGINE_VERSION: "16.10"
|
|
28
31
|
});
|
|
29
32
|
/**
|
|
30
33
|
* Bounds AWS enforces on `ServerlessV2ScalingConfiguration.SecondsUntilAutoPause`
|
|
@@ -17,6 +17,12 @@ interface SecretProps {
|
|
|
17
17
|
*/
|
|
18
18
|
secretStringValue?: string;
|
|
19
19
|
description?: string;
|
|
20
|
+
/**
|
|
21
|
+
* KMS alias for the wrapper-minted encryption CMK. Aliases are unique per
|
|
22
|
+
* account+region, so callers whose construct id is not account-unique (e.g.
|
|
23
|
+
* a fixed-name pattern deployed once per app) MUST pass an app-scoped alias.
|
|
24
|
+
* @default cmk/<construct id>
|
|
25
|
+
*/
|
|
20
26
|
aliasName?: string;
|
|
21
27
|
generateSecretString?: SecretStringGenerator;
|
|
22
28
|
/** Regions where this secret should be replicated (for Global Aurora) */
|
|
@@ -71,7 +71,7 @@ export class Secret extends Construct {
|
|
|
71
71
|
}
|
|
72
72
|
// Create KMS key for new secrets only
|
|
73
73
|
const customerManagedKey = new CustomerManagedKey(this, `${id}CustomerManagedKey`, {
|
|
74
|
-
aliasName: `cmk/${id}`
|
|
74
|
+
aliasName: props.aliasName ?? `cmk/${id}`
|
|
75
75
|
});
|
|
76
76
|
this.secretsCustomerManagedKey = customerManagedKey;
|
|
77
77
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.2",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
70
|
-
"@fjall/generator": "^3.
|
|
71
|
-
"@fjall/util": "^3.
|
|
70
|
+
"@fjall/generator": "^3.5.2",
|
|
71
|
+
"@fjall/util": "^3.5.2",
|
|
72
72
|
"constructs": "^10.6.0"
|
|
73
73
|
},
|
|
74
74
|
"overrides": {
|
|
@@ -82,5 +82,5 @@
|
|
|
82
82
|
"engines": {
|
|
83
83
|
"node": ">=18.0.0"
|
|
84
84
|
},
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "f59755647b2fd81ff692a3545e8e199d93c3bdf8"
|
|
86
86
|
}
|