@fjall/components-infrastructure 3.3.0 → 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/compute/ecsServiceFactory.d.ts +3 -3
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +60 -18
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +26 -2
- 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/domainCertificate.js +5 -2
- 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/acmTagValue.d.ts +2 -0
- package/dist/lib/utils/acmTagValue.js +13 -0
- package/dist/lib/utils/costAllocationTags.d.ts +2 -7
- package/dist/lib/utils/costAllocationTags.js +5 -7
- 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
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { FargateService, Ec2Service, AsgCapacityProvider, type DeploymentCircuitBreaker } from "aws-cdk-lib/aws-ecs";
|
|
2
2
|
import type { FargateTaskDefinition, Ec2TaskDefinition } from "aws-cdk-lib/aws-ecs";
|
|
3
3
|
import { type ISecurityGroup } from "aws-cdk-lib/aws-ec2";
|
|
4
|
-
import { TargetTrackingScalingPolicy
|
|
4
|
+
import { TargetTrackingScalingPolicy } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
5
5
|
import { type AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
|
|
6
|
-
import { type EcsServiceProps, type Ec2CapacityConfig } from "./ecsTypes.js";
|
|
6
|
+
import { type EcsServiceProps, type Ec2CapacityConfig, type QueueScalingResources } from "./ecsTypes.js";
|
|
7
7
|
import type { EcsConstructContext } from "./ecsContext.js";
|
|
8
8
|
/**
|
|
9
9
|
* Resolves the user's `circuitBreaker` config to the CDK
|
|
@@ -41,4 +41,4 @@ export declare function createService(ctx: EcsConstructContext, serviceName: str
|
|
|
41
41
|
* policy on utilisation; `QUEUE` emits a step-scaling policy on SQS backlog
|
|
42
42
|
* that can wake the service from `desiredCount: 0`.
|
|
43
43
|
*/
|
|
44
|
-
export declare function addServiceScaling(ctx: EcsConstructContext, serviceName: string, serviceProps: EcsServiceProps, service: FargateService | Ec2Service): TargetTrackingScalingPolicy |
|
|
44
|
+
export declare function addServiceScaling(ctx: EcsConstructContext, serviceName: string, serviceProps: EcsServiceProps, service: FargateService | Ec2Service): TargetTrackingScalingPolicy | QueueScalingResources;
|
|
@@ -3,8 +3,9 @@ import { Peer, Port, SubnetType, UserData } from "aws-cdk-lib/aws-ec2";
|
|
|
3
3
|
import { ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import { Role } from "../iam/role.js";
|
|
5
5
|
import { CfnOutput, Duration, Token } from "aws-cdk-lib";
|
|
6
|
-
import { AdjustmentType, PredefinedMetric, ScalableTarget, ServiceNamespace, TargetTrackingScalingPolicy } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
7
|
-
import { MathExpression } from "aws-cdk-lib/aws-cloudwatch";
|
|
6
|
+
import { AdjustmentType, PredefinedMetric, ScalableTarget, ServiceNamespace, StepScalingAction, TargetTrackingScalingPolicy } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
7
|
+
import { Alarm, ComparisonOperator, MathExpression } from "aws-cdk-lib/aws-cloudwatch";
|
|
8
|
+
import { ApplicationScalingAction } from "aws-cdk-lib/aws-cloudwatch-actions";
|
|
8
9
|
import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
|
|
9
10
|
import { SecurityGroup } from "../networking/securityGroup.js";
|
|
10
11
|
import { Ec2Instance } from "./ec2.js";
|
|
@@ -283,13 +284,14 @@ export function createService(ctx, serviceName, serviceProps, taskDefinition, as
|
|
|
283
284
|
return service;
|
|
284
285
|
}
|
|
285
286
|
/**
|
|
286
|
-
*
|
|
287
|
-
* `AdjustmentType.EXACT_CAPACITY` (`
|
|
288
|
-
* Backlog
|
|
289
|
-
*
|
|
287
|
+
* Wake-ladder intervals on the queue-backlog metric, with
|
|
288
|
+
* `AdjustmentType.EXACT_CAPACITY` (`capacity` is the absolute DesiredCount).
|
|
289
|
+
* Backlog ≥1 → 1 task; ≥100 → 2; ≥500 → 3. Capacities are clamped to the
|
|
290
|
+
* service's `maxCapacity` at build time. Bounds are absolute backlog values;
|
|
291
|
+
* `buildQueueScalingPolicy` re-expresses them relative to the wake alarm's
|
|
292
|
+
* threshold (the first interval's `lower`).
|
|
290
293
|
*/
|
|
291
|
-
const
|
|
292
|
-
{ lower: 0, upper: 1, capacity: 0 },
|
|
294
|
+
const QUEUE_WAKE_SCALING_STEPS = [
|
|
293
295
|
{ lower: 1, upper: 100, capacity: 1 },
|
|
294
296
|
{ lower: 100, upper: 500, capacity: 2 },
|
|
295
297
|
{ lower: 500, capacity: 3 }
|
|
@@ -327,11 +329,21 @@ export function addServiceScaling(ctx, serviceName, serviceProps, service) {
|
|
|
327
329
|
});
|
|
328
330
|
}
|
|
329
331
|
/**
|
|
330
|
-
* Builds the step-scaling
|
|
331
|
-
* `MathExpression` SUM of `ApproximateNumberOfMessagesVisible`
|
|
332
|
+
* Builds the wake/sleep step-scaling pair for `ScalingType.QUEUE`. The alarm
|
|
333
|
+
* metric is a `MathExpression` SUM of `ApproximateNumberOfMessagesVisible`
|
|
332
334
|
* (+`…NotVisible` when `includeInFlight`) across the consumed queues, sampled at
|
|
333
335
|
* `Maximum` over 1-minute periods — published independent of running-task count,
|
|
334
|
-
* so the
|
|
336
|
+
* so the alarms fire (and DesiredCount rises from 0) on backlog alone.
|
|
337
|
+
*
|
|
338
|
+
* The actions and alarms are wired explicitly rather than via
|
|
339
|
+
* `ScalableTarget.scaleOnMetric`: CDK's `StepScalingPolicy` middle-splits an
|
|
340
|
+
* all-`EXACT_CAPACITY` interval ladder to place its two alarm thresholds
|
|
341
|
+
* (`findAlarmThresholds` in aws-autoscaling-common), which for a
|
|
342
|
+
* 0/1/100/500 ladder puts BOTH thresholds at 100. Step policies are only
|
|
343
|
+
* invoked on alarm state transitions, so backlog 1–99 produced no transition
|
|
344
|
+
* and could never wake a zero-task service. The explicit pair pins the wake
|
|
345
|
+
* threshold at the ladder's first bound (backlog ≥ 1) and the sleep threshold
|
|
346
|
+
* at 0, making every 0↔1 backlog crossing a state transition.
|
|
335
347
|
*/
|
|
336
348
|
function buildQueueScalingPolicy(serviceName, config, scalableTarget, maxCapacity) {
|
|
337
349
|
if (config === undefined || config.queues.length === 0) {
|
|
@@ -363,16 +375,46 @@ function buildQueueScalingPolicy(serviceName, config, scalableTarget, maxCapacit
|
|
|
363
375
|
period: Duration.minutes(1),
|
|
364
376
|
label: `${serviceName}QueueBacklog`
|
|
365
377
|
});
|
|
366
|
-
|
|
378
|
+
const cooldown = config.cooldown ?? DEFAULT_QUEUE_SCALE_COOLDOWN;
|
|
379
|
+
const wakeThreshold = QUEUE_WAKE_SCALING_STEPS[0]?.lower ?? 1;
|
|
380
|
+
const wakeAction = new StepScalingAction(scalableTarget, `${serviceName}QueueWakeAction`, {
|
|
381
|
+
scalingTarget: scalableTarget,
|
|
382
|
+
adjustmentType: AdjustmentType.EXACT_CAPACITY,
|
|
383
|
+
cooldown
|
|
384
|
+
});
|
|
385
|
+
QUEUE_WAKE_SCALING_STEPS.forEach((step, index) => {
|
|
386
|
+
wakeAction.addAdjustment({
|
|
387
|
+
adjustment: Math.min(step.capacity, maxCapacity),
|
|
388
|
+
lowerBound: step.lower - wakeThreshold,
|
|
389
|
+
...(index < QUEUE_WAKE_SCALING_STEPS.length - 1 &&
|
|
390
|
+
step.upper !== undefined && { upperBound: step.upper - wakeThreshold })
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
const wakeAlarm = new Alarm(scalableTarget, `${serviceName}QueueWakeAlarm`, {
|
|
367
394
|
metric: backlogMetric,
|
|
395
|
+
alarmDescription: `Scale out '${serviceName}' on queue backlog`,
|
|
396
|
+
comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
|
|
397
|
+
threshold: wakeThreshold,
|
|
398
|
+
evaluationPeriods: 1,
|
|
399
|
+
datapointsToAlarm: 1
|
|
400
|
+
});
|
|
401
|
+
wakeAlarm.addAlarmAction(new ApplicationScalingAction(wakeAction));
|
|
402
|
+
const sleepAction = new StepScalingAction(scalableTarget, `${serviceName}QueueSleepAction`, {
|
|
403
|
+
scalingTarget: scalableTarget,
|
|
368
404
|
adjustmentType: AdjustmentType.EXACT_CAPACITY,
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
405
|
+
cooldown
|
|
406
|
+
});
|
|
407
|
+
// upperBound is relative to the alarm threshold (0), so this single
|
|
408
|
+
// open-ended interval covers exactly backlog ≤ 0.
|
|
409
|
+
sleepAction.addAdjustment({ adjustment: 0, upperBound: 0 });
|
|
410
|
+
const sleepAlarm = new Alarm(scalableTarget, `${serviceName}QueueSleepAlarm`, {
|
|
411
|
+
metric: backlogMetric,
|
|
412
|
+
alarmDescription: `Scale '${serviceName}' to zero on empty queue`,
|
|
413
|
+
comparisonOperator: ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD,
|
|
414
|
+
threshold: 0,
|
|
375
415
|
evaluationPeriods: 1,
|
|
376
416
|
datapointsToAlarm: 1
|
|
377
417
|
});
|
|
418
|
+
sleepAlarm.addAlarmAction(new ApplicationScalingAction(sleepAction));
|
|
419
|
+
return { wakeAction, wakeAlarm, sleepAction, sleepAlarm };
|
|
378
420
|
}
|
|
@@ -4,7 +4,8 @@ import { type Monitoring } from "aws-cdk-lib/aws-autoscaling";
|
|
|
4
4
|
import { type IService } from "aws-cdk-lib/aws-servicediscovery";
|
|
5
5
|
import { type IManagedPolicy, type PolicyDocument } from "aws-cdk-lib/aws-iam";
|
|
6
6
|
import type { DockerBuild } from "@fjall/util/manifest/schemas";
|
|
7
|
-
import { type TargetTrackingScalingPolicy, type
|
|
7
|
+
import { type TargetTrackingScalingPolicy, type StepScalingAction } from "aws-cdk-lib/aws-applicationautoscaling";
|
|
8
|
+
import { type Alarm } from "aws-cdk-lib/aws-cloudwatch";
|
|
8
9
|
import { type IQueue } from "aws-cdk-lib/aws-sqs";
|
|
9
10
|
import { type Duration } from "aws-cdk-lib";
|
|
10
11
|
import { type GeoLocation } from "aws-cdk-lib/aws-route53";
|
|
@@ -63,6 +64,29 @@ export interface QueueScalingConfig {
|
|
|
63
64
|
/** Cooldown after a scale action. Default: 5 minutes. */
|
|
64
65
|
cooldown?: Duration;
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The explicitly-wired wake/sleep alarm pair emitted for `ScalingType.QUEUE`.
|
|
69
|
+
*
|
|
70
|
+
* Built by hand (NOT via `ScalableTarget.scaleOnMetric`) because CDK's
|
|
71
|
+
* `StepScalingPolicy` places its two alarm thresholds by middle-splitting the
|
|
72
|
+
* interval ladder when every `EXACT_CAPACITY` step carries a change — for the
|
|
73
|
+
* default 4-step ladder that puts BOTH thresholds at backlog 100, and since
|
|
74
|
+
* step policies are only invoked on alarm state transitions, a backlog of
|
|
75
|
+
* 1–99 can never wake a `desiredCount: 0` service (observed live: the prod
|
|
76
|
+
* deploy-worker sat at 0 tasks with 1 queued message indefinitely). The
|
|
77
|
+
* explicit pair pins the wake boundary at backlog ≥ 1 and the sleep boundary
|
|
78
|
+
* at backlog 0.
|
|
79
|
+
*/
|
|
80
|
+
export interface QueueScalingResources {
|
|
81
|
+
/** Scale-out action: backlog ≥ 1 → capacity ladder (1/2/3, clamped). */
|
|
82
|
+
wakeAction: StepScalingAction;
|
|
83
|
+
/** Alarm at backlog ≥ 1 driving `wakeAction`. */
|
|
84
|
+
wakeAlarm: Alarm;
|
|
85
|
+
/** Scale-in action: backlog 0 → capacity 0. */
|
|
86
|
+
sleepAction: StepScalingAction;
|
|
87
|
+
/** Alarm at backlog ≤ 0 driving `sleepAction`. */
|
|
88
|
+
sleepAlarm: Alarm;
|
|
89
|
+
}
|
|
66
90
|
import type { EcsCapacityProvider } from "@fjall/generator";
|
|
67
91
|
export type { EcsCapacityProvider };
|
|
68
92
|
/**
|
|
@@ -607,7 +631,7 @@ export interface ServiceData {
|
|
|
607
631
|
containers: ContainerDefinition[];
|
|
608
632
|
primaryContainer?: ContainerDefinition;
|
|
609
633
|
targetGroup?: IApplicationTargetGroup;
|
|
610
|
-
scalingPolicy?: TargetTrackingScalingPolicy |
|
|
634
|
+
scalingPolicy?: TargetTrackingScalingPolicy | QueueScalingResources;
|
|
611
635
|
/** Explicit log group shared by all of the service's containers. */
|
|
612
636
|
logGroup: ILogGroup;
|
|
613
637
|
}
|
|
@@ -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`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CfnOutput, Tags } from "aws-cdk-lib";
|
|
2
2
|
import { Certificate, CertificateValidation } from "aws-cdk-lib/aws-certificatemanager";
|
|
3
3
|
import { getDomainExportNames } from "@fjall/util";
|
|
4
|
+
import { sanitiseAcmTagValue } from "../../../utils/acmTagValue.js";
|
|
4
5
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
5
6
|
import { applyCostAllocationTags } from "../../../utils/costAllocationTags.js";
|
|
6
7
|
/**
|
|
@@ -33,10 +34,12 @@ export class DomainCertificate extends Certificate {
|
|
|
33
34
|
this.description =
|
|
34
35
|
props.description ??
|
|
35
36
|
`Fjall-managed ACM certificate for ${props.domainName}`;
|
|
36
|
-
|
|
37
|
+
// ACM's tag-value charset excludes `*`, so a wildcard domainName in either
|
|
38
|
+
// tag fails CreateCertificate — sanitise both (see utils/acmTagValue.ts).
|
|
39
|
+
Tags.of(this).add("fjall:description", sanitiseAcmTagValue(this.description));
|
|
37
40
|
applyCostAllocationTags(this, {
|
|
38
41
|
service: "certificate",
|
|
39
|
-
domain: props.costAllocationDomain ?? props.domainName,
|
|
42
|
+
domain: sanitiseAcmTagValue(props.costAllocationDomain ?? props.domainName),
|
|
40
43
|
environment: props.costAllocationEnvironment,
|
|
41
44
|
inheritEnvironment: props.inheritCostAllocationEnvironment
|
|
42
45
|
});
|
|
@@ -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
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACM rejects tag values containing characters outside
|
|
3
|
+
* `[\p{L}\p{Z}\p{N}_.:/=+\-@]*` (letters, spaces, numbers, `_ . : / = + - @`)
|
|
4
|
+
* with a CreateCertificate ValidationException — notably `*`, so any tag
|
|
5
|
+
* embedding a wildcard domain (`*.dev.example.com`) fails the whole stack at
|
|
6
|
+
* deploy time. Most other taggable services (S3, EC2, Route53) accept `*`,
|
|
7
|
+
* which is why the constraint only surfaces at the certificate boundary.
|
|
8
|
+
*/
|
|
9
|
+
const ACM_TAG_VALUE_DISALLOWED = /[^\p{L}\p{Z}\p{N}_.:/=+\-@]/gu;
|
|
10
|
+
/** Replace every ACM-disallowed character with `_` (an allowed character). */
|
|
11
|
+
export function sanitiseAcmTagValue(value) {
|
|
12
|
+
return value.replace(ACM_TAG_VALUE_DISALLOWED, "_");
|
|
13
|
+
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
+
import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "@fjall/util/aws";
|
|
1
2
|
import type { IConstruct } from "constructs";
|
|
2
|
-
export
|
|
3
|
-
readonly ENVIRONMENT: "fjall:costAllocation:environment";
|
|
4
|
-
readonly SERVICE: "fjall:costAllocation:service";
|
|
5
|
-
readonly DOMAIN: "fjall:costAllocation:domain";
|
|
6
|
-
readonly OWNER: "fjall:costAllocation:owner";
|
|
7
|
-
};
|
|
8
|
-
export declare const DEFAULT_COST_ALLOCATION_ENVIRONMENT: "management";
|
|
3
|
+
export { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT };
|
|
9
4
|
export interface CostAllocationTagsArgs {
|
|
10
5
|
readonly service: string;
|
|
11
6
|
readonly domain: string;
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
+
import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "@fjall/util/aws";
|
|
1
2
|
import { Tags } from "aws-cdk-lib";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
OWNER: "fjall:costAllocation:owner"
|
|
7
|
-
};
|
|
8
|
-
export const DEFAULT_COST_ALLOCATION_ENVIRONMENT = "management";
|
|
3
|
+
// Canonical home: @fjall/util (`util/src/aws/costAllocationTags.ts`) — shared
|
|
4
|
+
// with deploy-core's `cdk bootstrap --tags` stamping. Re-exported here so
|
|
5
|
+
// construct-package consumers keep their existing import path.
|
|
6
|
+
export { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT };
|
|
9
7
|
export function applyCostAllocationTags(scope, args) {
|
|
10
8
|
if (args.environment !== undefined) {
|
|
11
9
|
Tags.of(scope).add(COST_ALLOCATION_TAGS.ENVIRONMENT, args.environment);
|
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
|
}
|