@fjall/components-infrastructure 7.3.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/app.d.ts +12 -0
- package/dist/lib/app.js +25 -5
- package/dist/lib/config/aws/ipam.js +4 -3
- package/dist/lib/patterns/aws/cdn.d.ts +7 -1
- package/dist/lib/patterns/aws/cdn.js +9 -2
- package/dist/lib/patterns/aws/interfaces/domain.d.ts +6 -0
- package/dist/lib/patterns/aws/storage.js +3 -0
- package/dist/lib/patterns/aws/targets/fjallTargets.d.ts +9 -4
- package/dist/lib/patterns/aws/targets/fjallTargets.js +11 -5
- package/dist/lib/patterns/aws/targets/targetResolution.d.ts +33 -40
- package/dist/lib/patterns/aws/targets/targetResolution.js +42 -52
- package/dist/lib/resources/aws/backup/backupVault.js +2 -0
- package/dist/lib/resources/aws/cdn/cloudFront.d.ts +9 -0
- package/dist/lib/resources/aws/cdn/cloudFront.js +2 -1
- package/dist/lib/resources/aws/compute/ecs.js +4 -3
- package/dist/lib/resources/aws/compute/ecsNetworking.js +27 -9
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +2 -1
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +6 -2
- package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +2 -1
- package/dist/lib/resources/aws/database/rdsHelpers.js +12 -2
- package/dist/lib/resources/aws/database/rdsInstance.js +2 -1
- package/dist/lib/resources/aws/database/rdsProxyOutput.js +2 -1
- package/dist/lib/resources/aws/logging/cloudTrail.js +3 -0
- package/dist/lib/resources/aws/networking/ipamPool.js +2 -1
- package/dist/lib/resources/aws/networking/vpc.js +6 -1
- package/dist/lib/resources/aws/secrets/kms.d.ts +24 -7
- package/dist/lib/resources/aws/secrets/kms.js +27 -5
- package/dist/lib/resources/aws/secrets/parameter.js +3 -1
- package/dist/lib/resources/aws/secrets/secret.js +4 -1
- package/dist/lib/resources/aws/storage/ecr.d.ts +6 -0
- package/dist/lib/resources/aws/storage/ecr.js +8 -4
- package/dist/lib/resources/aws/storage/s3.d.ts +10 -0
- package/dist/lib/resources/aws/storage/s3.js +9 -4
- package/dist/lib/utils/albAliasTargetRegistry.d.ts +44 -0
- package/dist/lib/utils/albAliasTargetRegistry.js +69 -0
- package/dist/lib/utils/env.d.ts +9 -0
- package/dist/lib/utils/env.js +12 -0
- package/dist/lib/utils/exportNaming.d.ts +50 -0
- package/dist/lib/utils/exportNaming.js +52 -0
- package/dist/lib/utils/orgConfigParser.d.ts +3 -8
- package/dist/lib/utils/orgConfigParser.js +4 -14
- package/dist/lib/utils/removalPolicy.d.ts +11 -3
- package/dist/lib/utils/removalPolicy.js +20 -5
- package/package.json +5 -4
|
@@ -11,6 +11,9 @@ import { SecurityGroup } from "../networking/securityGroup.js";
|
|
|
11
11
|
import { DNS_APEX, isManagedDomainBinding, isWithinZone } from "../../../utils/domainTypes.js";
|
|
12
12
|
import { readInjectedManagedDomainBinding } from "../../../utils/managedDomainContext.js";
|
|
13
13
|
import { ResourceNaming } from "../../../utils/resourceNaming.js";
|
|
14
|
+
import { stackScopedExportName } from "../../../utils/exportNaming.js";
|
|
15
|
+
import { registerAlbAliasTarget } from "../../../utils/albAliasTargetRegistry.js";
|
|
16
|
+
import { albDnsExportName, albHostedZoneIdExportName } from "@fjall/util";
|
|
14
17
|
import { getSafeZoneName, toPascalCase } from "../../../utils/capitaliseString.js";
|
|
15
18
|
import { FjallLogger } from "../../../utils/validationLogger.js";
|
|
16
19
|
import { isServiceEc2 } from "./ecsTaskDefinition.js";
|
|
@@ -44,27 +47,42 @@ export function addLoadBalancer(ctx, anyServiceUsesEc2, asgSecurityGroup) {
|
|
|
44
47
|
});
|
|
45
48
|
new CfnOutput(ctx.scope, `${ctx.outputName}LoadBalancerDnsName`, {
|
|
46
49
|
key: `${ctx.outputName}LoadBalancerDnsName`,
|
|
47
|
-
exportName: `${props.clusterName}LoadBalancerDnsName
|
|
50
|
+
exportName: stackScopedExportName(ctx.scope, `${props.clusterName}LoadBalancerDnsName`),
|
|
48
51
|
value: loadBalancer.loadBalancerDnsName
|
|
49
52
|
});
|
|
50
|
-
// Cross-stack exports consumed by the Domain construct's
|
|
51
|
-
// alias target.
|
|
53
|
+
// Cross-stack exports consumed by the Domain construct's
|
|
54
|
+
// `fjallApp(<app>, <compute>)` alias target. Keyed on the APP name — not
|
|
55
|
+
// `ctx.outputName` (the compute construct id) — because `fjall add compute`
|
|
56
|
+
// names the compute from the user's --name, so an app "my-app" with a
|
|
57
|
+
// compute "api" exported `ApiAlbDnsName` while the Domain asked for
|
|
58
|
+
// `MyAppAlbDnsName`. Qualified by the compute too, because an app may front
|
|
59
|
+
// several ALBs and the app-keyed name alone collides; the unqualified
|
|
60
|
+
// app-level pair is emitted once by App.synth, and only when there is
|
|
61
|
+
// exactly one (albAliasTargetRegistry.ts).
|
|
62
|
+
const targetAppName = props.appName ?? props.clusterName;
|
|
63
|
+
registerAlbAliasTarget({
|
|
64
|
+
appName: targetAppName,
|
|
65
|
+
computeName: props.clusterName,
|
|
66
|
+
scope: ctx.scope,
|
|
67
|
+
dnsName: loadBalancer.loadBalancerDnsName,
|
|
68
|
+
hostedZoneId: loadBalancer.loadBalancerCanonicalHostedZoneId
|
|
69
|
+
});
|
|
52
70
|
new CfnOutput(ctx.scope, `${ctx.outputName}AlbDnsName`, {
|
|
53
71
|
key: `${ctx.outputName}AlbDnsName`,
|
|
54
|
-
exportName:
|
|
72
|
+
exportName: albDnsExportName(targetAppName, props.clusterName),
|
|
55
73
|
value: loadBalancer.loadBalancerDnsName,
|
|
56
74
|
description: `ALB DNS name for ${props.clusterName} (consumed by Domain alias targets)`
|
|
57
75
|
});
|
|
58
76
|
new CfnOutput(ctx.scope, `${ctx.outputName}AlbHostedZoneId`, {
|
|
59
77
|
key: `${ctx.outputName}AlbHostedZoneId`,
|
|
60
|
-
exportName:
|
|
78
|
+
exportName: albHostedZoneIdExportName(targetAppName, props.clusterName),
|
|
61
79
|
value: loadBalancer.loadBalancerCanonicalHostedZoneId,
|
|
62
80
|
description: `ALB canonical hosted zone ID for ${props.clusterName}`
|
|
63
81
|
});
|
|
64
82
|
const customDomain = props.cluster?.domain || props.cluster?.domainConfig?.domainName;
|
|
65
83
|
new CfnOutput(ctx.scope, `${ctx.outputName}LoadBalancerUrl`, {
|
|
66
84
|
key: `${ctx.outputName}LoadBalancerUrl`,
|
|
67
|
-
exportName: `${props.clusterName}LoadBalancerUrl
|
|
85
|
+
exportName: stackScopedExportName(ctx.scope, `${props.clusterName}LoadBalancerUrl`),
|
|
68
86
|
value: customDomain
|
|
69
87
|
? `https://${customDomain}`
|
|
70
88
|
: `http://${loadBalancer.loadBalancerDnsName}`,
|
|
@@ -73,7 +91,7 @@ export function addLoadBalancer(ctx, anyServiceUsesEc2, asgSecurityGroup) {
|
|
|
73
91
|
// Export load balancer ARN for monitoring
|
|
74
92
|
new CfnOutput(ctx.scope, `${ctx.outputName}LoadBalancerArn`, {
|
|
75
93
|
key: `${ctx.outputName}LoadBalancerArn`,
|
|
76
|
-
exportName: `${props.clusterName}LoadBalancerArn
|
|
94
|
+
exportName: stackScopedExportName(ctx.scope, `${props.clusterName}LoadBalancerArn`),
|
|
77
95
|
value: loadBalancer.loadBalancerArn,
|
|
78
96
|
description: `Load Balancer ARN for ${props.clusterName}`
|
|
79
97
|
});
|
|
@@ -469,13 +487,13 @@ export function addDirectAccessOutputs(ctx, autoScalingGroup) {
|
|
|
469
487
|
3000;
|
|
470
488
|
new CfnOutput(ctx.scope, `${ctx.outputName}AutoScalingGroupName`, {
|
|
471
489
|
key: `${ctx.outputName}AutoScalingGroupName`,
|
|
472
|
-
exportName: `${props.clusterName}AutoScalingGroupName
|
|
490
|
+
exportName: stackScopedExportName(ctx.scope, `${props.clusterName}AutoScalingGroupName`),
|
|
473
491
|
value: autoScalingGroup.autoScalingGroupName,
|
|
474
492
|
description: `Run: aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names <name> to find instance IP`
|
|
475
493
|
});
|
|
476
494
|
new CfnOutput(ctx.scope, `${ctx.outputName}DirectAccessPort`, {
|
|
477
495
|
key: `${ctx.outputName}DirectAccessPort`,
|
|
478
|
-
exportName: `${props.clusterName}DirectAccessPort
|
|
496
|
+
exportName: stackScopedExportName(ctx.scope, `${props.clusterName}DirectAccessPort`),
|
|
479
497
|
value: String(containerPort),
|
|
480
498
|
description: `Access your app at http://<EC2-PUBLIC-IP>:${containerPort}`
|
|
481
499
|
});
|
|
@@ -10,6 +10,7 @@ import { SecurityGroup } from "../networking/securityGroup.js";
|
|
|
10
10
|
import { Ec2Instance } from "./ec2.js";
|
|
11
11
|
import { vpcHasNatGateways } from "../../../utils/vpcUtils.js";
|
|
12
12
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
13
|
+
import { stackScopedExportName } from "../../../utils/exportNaming.js";
|
|
13
14
|
import { DEFAULT_EC2_INSTANCE_TYPE, DEFAULT_EC2_MIN_CAPACITY, DEFAULT_EC2_MAX_CAPACITY, DEFAULT_EC2_INSTANCE_MONITORING, DEFAULT_WARM_POOL_MIN_SIZE, DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN, DEFAULT_HEALTH_CHECK_GRACE_SECONDS, DEFAULT_MIN_HEALTHY_PERCENT, DEFAULT_MAX_HEALTHY_PERCENT, DEFAULT_DESIRED_COUNT, inferAmiHardwareType } from "./ecsConstants.js";
|
|
14
15
|
import { assertAnchorUnique, assertSharedAsgConfigMatches, assertValidCapacitySlot, getEc2ConfigKey, legacyAnchorFromKey, LEGACY_KEY_FIELDS, resolveCapacityAnchor, resolveCapacitySlot } from "./ecsCapacityConfig.js";
|
|
15
16
|
import { lookupCapacityIdentityPin } from "../../../utils/capacityIdentityContext.js";
|
|
@@ -308,7 +309,7 @@ export function createService(ctx, serviceName, serviceProps, taskDefinition, as
|
|
|
308
309
|
}
|
|
309
310
|
new CfnOutput(ctx.scope, `${ctx.outputName}${toPascalCase(serviceName)}ServiceArn`, {
|
|
310
311
|
key: `${ctx.outputName}${toPascalCase(serviceName)}ServiceArn`,
|
|
311
|
-
exportName: `${ctx.props.clusterName}${serviceName}ServiceArn
|
|
312
|
+
exportName: stackScopedExportName(ctx.scope, `${ctx.props.clusterName}${serviceName}ServiceArn`),
|
|
312
313
|
value: service.serviceArn,
|
|
313
314
|
description: `ECS Service ARN for ${serviceName}`
|
|
314
315
|
});
|
|
@@ -159,9 +159,13 @@ export function addContainersToTask(ctx, serviceName, serviceProps, taskDefiniti
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
if (containerConfig.secrets && containerConfig.secrets.length > 0) {
|
|
162
|
+
// `secrets` is a set of names: a repeated entry (hand-merged arrays,
|
|
163
|
+
// shared-const spread overlap) must not construct a second SSM import —
|
|
164
|
+
// the construct id embeds the name, and a duplicate id fails synth.
|
|
165
|
+
const uniqueSecretNames = [...new Set(containerConfig.secrets)];
|
|
162
166
|
if (containerConfig.secretsImport) {
|
|
163
167
|
const secretsImportKeys = Object.keys(containerConfig.secretsImport);
|
|
164
|
-
const duplicateKeys =
|
|
168
|
+
const duplicateKeys = uniqueSecretNames.filter((key) => secretsImportKeys.includes(key));
|
|
165
169
|
if (duplicateKeys.length > 0) {
|
|
166
170
|
throw new Error(`Container '${containerConfig.name}' in service '${serviceName}' has duplicate secret keys ` +
|
|
167
171
|
`defined in both secrets and secretsImport: ${duplicateKeys.join(", ")}. ` +
|
|
@@ -169,7 +173,7 @@ export function addContainersToTask(ctx, serviceName, serviceProps, taskDefiniti
|
|
|
169
173
|
}
|
|
170
174
|
}
|
|
171
175
|
const ssmSecretsPath = deriveSsmSecretsPath(ctx.props, serviceName, serviceProps.ssmSecretsPath);
|
|
172
|
-
for (const secretName of
|
|
176
|
+
for (const secretName of uniqueSecretNames) {
|
|
173
177
|
validateSecretName(secretName, `container '${containerConfig.name}' of service '${serviceName}'`);
|
|
174
178
|
const paramPath = `${ssmSecretsPath}/${secretName}`;
|
|
175
179
|
const param = StringParameter.fromSecureStringParameterAttributes(ctx.scope, `${ctx.props.clusterName}${serviceName}${containerConfig.name}${secretName}SsmParam`, { parameterName: paramPath });
|
|
@@ -3,6 +3,7 @@ import { CfnGlobalCluster } from "aws-cdk-lib/aws-rds";
|
|
|
3
3
|
import { Construct } from "constructs";
|
|
4
4
|
import { RdsAurora } from "./rdsAurora.js";
|
|
5
5
|
import { ResourceNaming } from "../../../utils/resourceNaming.js";
|
|
6
|
+
import { stackScopedExportName } from "../../../utils/exportNaming.js";
|
|
6
7
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
7
8
|
export class RdsAuroraGlobal extends Construct {
|
|
8
9
|
connections;
|
|
@@ -133,7 +134,7 @@ export class RdsAuroraGlobal extends Construct {
|
|
|
133
134
|
const outputName = toPascalCase(this.props.databaseName);
|
|
134
135
|
new CfnOutput(this, `${outputName}GlobalClusterIdentifierOutput`, {
|
|
135
136
|
value: this.globalId,
|
|
136
|
-
exportName: `${outputName}GlobalClusterIdentifier`
|
|
137
|
+
exportName: stackScopedExportName(this, `${outputName}GlobalClusterIdentifier`)
|
|
137
138
|
});
|
|
138
139
|
}
|
|
139
140
|
getCredentials() {
|
|
@@ -32,7 +32,13 @@ const MULTI_USER_ROTATION_APPLICATIONS = {
|
|
|
32
32
|
*/
|
|
33
33
|
export function resolveStorageEncryptionKey(scope, databaseName, storageKey) {
|
|
34
34
|
if (isCMKRequested(storageKey)) {
|
|
35
|
-
return new CustomerManagedKey(scope, `${databaseName}ClusterEncryptionKey`, {
|
|
35
|
+
return new CustomerManagedKey(scope, `${databaseName}ClusterEncryptionKey`, {
|
|
36
|
+
aliasName: ResourceNaming.storageEncryptionKeyAlias(databaseName),
|
|
37
|
+
// RDS is RemovalPolicy.SNAPSHOT, so deleting the stack leaves a final
|
|
38
|
+
// snapshot behind. Destroying its storage key would make that snapshot
|
|
39
|
+
// permanently unrestorable.
|
|
40
|
+
protects: "outlives-stack"
|
|
41
|
+
}).key;
|
|
36
42
|
}
|
|
37
43
|
if (isAwsManagedKey(storageKey) || storageKey === undefined) {
|
|
38
44
|
return undefined;
|
|
@@ -45,7 +51,11 @@ export function resolveStorageEncryptionKey(scope, databaseName, storageKey) {
|
|
|
45
51
|
*/
|
|
46
52
|
export function resolvePerformanceInsightsKey(scope, databaseName, piEnabled, encryptionKey) {
|
|
47
53
|
if (piEnabled && isCMKRequested(encryptionKey)) {
|
|
48
|
-
return new CustomerManagedKey(scope, `${databaseName}PerformanceInsightsKey`, {
|
|
54
|
+
return new CustomerManagedKey(scope, `${databaseName}PerformanceInsightsKey`, {
|
|
55
|
+
aliasName: ResourceNaming.insightsKeyAlias(databaseName),
|
|
56
|
+
// Performance-insights data dies with the instance.
|
|
57
|
+
protects: "stack-scoped"
|
|
58
|
+
}).key;
|
|
49
59
|
}
|
|
50
60
|
return undefined;
|
|
51
61
|
}
|
|
@@ -237,7 +237,8 @@ export class RdsInstance extends Construct {
|
|
|
237
237
|
const { piEnabled, piConfig } = resolveDatabaseInsights(props.databaseInsights);
|
|
238
238
|
const readReplicaPerformanceInsightsKey = piEnabled && isCMKRequested(piConfig?.encryptionKey)
|
|
239
239
|
? new CustomerManagedKey(this, `${this.databaseNameValue}ReadReplicaReaderInsightsKey`, {
|
|
240
|
-
aliasName: ResourceNaming.readReplicaInsightsKeyAlias(this.databaseNameValue)
|
|
240
|
+
aliasName: ResourceNaming.readReplicaInsightsKeyAlias(this.databaseNameValue),
|
|
241
|
+
protects: "stack-scoped"
|
|
241
242
|
}).key
|
|
242
243
|
: undefined;
|
|
243
244
|
this.readReplicaSecurityGroup = new SecurityGroup(this, `${this.databaseNameValue}ReadReplicaSecurityGroup`, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CfnOutput } from "aws-cdk-lib";
|
|
2
2
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
3
|
+
import { stackScopedExportName } from "../../../utils/exportNaming.js";
|
|
3
4
|
/**
|
|
4
5
|
* Add a CfnOutput for the RDS proxy endpoint.
|
|
5
6
|
* Shared by RdsAurora and RdsInstance constructs.
|
|
@@ -8,7 +9,7 @@ export function addProxyCfnOutput(scope, constructId, databaseName, proxy) {
|
|
|
8
9
|
const outputName = toPascalCase(databaseName);
|
|
9
10
|
new CfnOutput(scope, `${outputName}ProxyEndpointOutput`, {
|
|
10
11
|
key: `${outputName}ProxyEndpoint`,
|
|
11
|
-
exportName: `${outputName}ProxyEndpoint
|
|
12
|
+
exportName: stackScopedExportName(scope, `${outputName}ProxyEndpoint`),
|
|
12
13
|
value: proxy.endpoint
|
|
13
14
|
});
|
|
14
15
|
}
|
|
@@ -29,6 +29,9 @@ export class Trail extends Construct {
|
|
|
29
29
|
? RemovalPolicy.RETAIN
|
|
30
30
|
: RemovalPolicy.DESTROY;
|
|
31
31
|
this.encryptionKey = new CustomerManagedKey(this, `${id}CloudTrailEncryptionKey`, {
|
|
32
|
+
// Bound to the bucket's own policy below — the trail's log objects
|
|
33
|
+
// outlive the stack exactly when the bucket does.
|
|
34
|
+
protects: "outlives-stack",
|
|
32
35
|
aliasName: `cmk/cloudtrail/${id}/encryptionKey`,
|
|
33
36
|
removalPolicy: storagePolicy
|
|
34
37
|
});
|
|
@@ -6,6 +6,7 @@ import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
|
6
6
|
import { ResourceShare } from "../utilities/resourceShare.js";
|
|
7
7
|
import { CustomResource } from "../utilities/customResource.js";
|
|
8
8
|
import getAccountId from "../../../utils/getAccountId.js";
|
|
9
|
+
import { IPAM_PRIVATE_DEFAULT_SCOPE_EXPORT_NAME } from "../../../utils/exportNaming.js";
|
|
9
10
|
import { accountConstructKey, findAccountNameCollision } from "../../../utils/capitaliseString.js";
|
|
10
11
|
import { FjallLogger } from "../../../utils/validationLogger.js";
|
|
11
12
|
import { formatIpamPairTagValue, IPAM_OPERATIONS_POOL_TAG_KEY } from "@fjall/util/aws";
|
|
@@ -20,7 +21,7 @@ export class IpamPool extends Construct {
|
|
|
20
21
|
constructor(scope, id, props) {
|
|
21
22
|
super(scope, id);
|
|
22
23
|
// Get the default IPAM scope ID from props, or CFN import
|
|
23
|
-
const ipamScopeId = props.ipamScope || Fn.importValue(
|
|
24
|
+
const ipamScopeId = props.ipamScope || Fn.importValue(IPAM_PRIVATE_DEFAULT_SCOPE_EXPORT_NAME);
|
|
24
25
|
const regions = props.regions;
|
|
25
26
|
if (!regions || regions.length === 0) {
|
|
26
27
|
throw new Error("At least one region must be specified for IPAM pools");
|
|
@@ -27,9 +27,14 @@ export class Vpc extends ec2.Vpc {
|
|
|
27
27
|
});
|
|
28
28
|
this.hasNatGateways = natGateways !== 0;
|
|
29
29
|
this.addInterfaceEndpoints(id, props, this.hasNatGateways);
|
|
30
|
+
// Export names are unique per account+region, so the id is load-bearing:
|
|
31
|
+
// without it a stack holding two VPCs emits the name twice and
|
|
32
|
+
// CloudFormation rejects the template. The `-vpc-cidr` suffix must stay
|
|
33
|
+
// last — the webapp finds a linked app's CIDR by ListExports + suffix
|
|
34
|
+
// filter (webapp app/.server/services/infrastructure/captureVpcCidr.ts).
|
|
30
35
|
new CfnOutput(this, "VpcCidrBlock", {
|
|
31
36
|
value: this.vpcCidrBlock,
|
|
32
|
-
exportName: `${Stack.of(this).stackName}-vpc-cidr`
|
|
37
|
+
exportName: `${Stack.of(this).stackName}${id}-vpc-cidr`
|
|
33
38
|
});
|
|
34
39
|
}
|
|
35
40
|
static gatewayEndpoints(props) {
|
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import { RemovalPolicy } from "aws-cdk-lib";
|
|
2
2
|
import { Alias, Key } from "aws-cdk-lib/aws-kms";
|
|
3
3
|
import { Construct } from "constructs";
|
|
4
|
+
export declare const KEY_PROTECTION_SCOPES: readonly ["stack-scoped", "outlives-stack"];
|
|
5
|
+
export type KeyProtectionScope = (typeof KEY_PROTECTION_SCOPES)[number];
|
|
4
6
|
interface CustomerManagedKeyProps {
|
|
5
7
|
description?: string;
|
|
6
8
|
aliasName?: string;
|
|
7
9
|
/**
|
|
8
|
-
*
|
|
10
|
+
* Whether anything this key encrypts survives deletion of the stack that
|
|
11
|
+
* created it. Required, and deliberately not defaulted: a key destroyed
|
|
12
|
+
* while a resource it encrypted still exists makes that resource
|
|
13
|
+
* permanently unreadable, and nothing downstream can detect it.
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
15
|
+
* - `"stack-scoped"` — everything the key encrypts goes with the stack (an
|
|
16
|
+
* SSM parameter, a Secrets Manager secret, performance-insights data).
|
|
17
|
+
* The key follows the env-aware default (D17): production RETAINs, every
|
|
18
|
+
* other recognised stage DESTROYs. This is what stops a torn-down dev
|
|
19
|
+
* stack leaving a chargeable key behind forever.
|
|
20
|
+
* - `"outlives-stack"` — at least one encrypted resource survives (an RDS
|
|
21
|
+
* final snapshot under `RemovalPolicy.SNAPSHOT`, a retained audit bucket,
|
|
22
|
+
* a backup vault). The key RETAINs in every environment: ~$1/month is not
|
|
23
|
+
* a reason to render a surviving snapshot unrestorable.
|
|
13
24
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
25
|
+
* A caller that knows better may still override with `removalPolicy` — the
|
|
26
|
+
* teardown paths that delete the survivor first legitimately do.
|
|
27
|
+
*/
|
|
28
|
+
readonly protects: KeyProtectionScope;
|
|
29
|
+
/**
|
|
30
|
+
* Explicit removal policy, overriding what `protects` would resolve to.
|
|
16
31
|
*
|
|
17
|
-
*
|
|
32
|
+
* Prefer binding it to the policy of the thing being encrypted rather than
|
|
33
|
+
* choosing independently — `CloudTrail` passes the same `storagePolicy` to
|
|
34
|
+
* its bucket and its key precisely so the two cannot drift apart.
|
|
18
35
|
*/
|
|
19
36
|
removalPolicy?: RemovalPolicy;
|
|
20
37
|
}
|
|
@@ -3,6 +3,22 @@ import { Alias, Key } from "aws-cdk-lib/aws-kms";
|
|
|
3
3
|
import { Construct } from "constructs";
|
|
4
4
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
5
5
|
import { ResourceNaming } from "../../../utils/resourceNaming.js";
|
|
6
|
+
import { stackScopedExportName } from "../../../utils/exportNaming.js";
|
|
7
|
+
import { envAwareRemovalPolicyDefault, toRemovalPolicy } from "../../../utils/removalPolicy.js";
|
|
8
|
+
export const KEY_PROTECTION_SCOPES = [
|
|
9
|
+
"stack-scoped",
|
|
10
|
+
"outlives-stack"
|
|
11
|
+
];
|
|
12
|
+
/**
|
|
13
|
+
* How long a scheduled-for-deletion key stays recoverable.
|
|
14
|
+
*
|
|
15
|
+
* The window must cover the longest recovery affordance of anything the key
|
|
16
|
+
* protects, or the survivor becomes unreadable before its own recovery period
|
|
17
|
+
* expires. The longest in this codebase is Secrets Manager's 30-day recovery
|
|
18
|
+
* window, so the key matches it. (30 days is also AWS's own default and its
|
|
19
|
+
* maximum; the minimum is 7.)
|
|
20
|
+
*/
|
|
21
|
+
const PENDING_DELETION_WINDOW = Duration.days(30);
|
|
6
22
|
export class CustomerManagedKey extends Construct {
|
|
7
23
|
key;
|
|
8
24
|
alias;
|
|
@@ -10,17 +26,23 @@ export class CustomerManagedKey extends Construct {
|
|
|
10
26
|
super(scope, id);
|
|
11
27
|
// Sanitise id for CloudFormation output keys (must be alphanumeric)
|
|
12
28
|
const outputName = toPascalCase(id);
|
|
29
|
+
const removalPolicy = props.removalPolicy ??
|
|
30
|
+
(props.protects === "outlives-stack"
|
|
31
|
+
? RemovalPolicy.RETAIN
|
|
32
|
+
: toRemovalPolicy(envAwareRemovalPolicyDefault()));
|
|
13
33
|
this.key = new Key(this, `${id}Key`, {
|
|
14
34
|
description: props.description || `${id} KMS Key`,
|
|
15
|
-
removalPolicy
|
|
16
|
-
|
|
17
|
-
|
|
35
|
+
removalPolicy,
|
|
36
|
+
// Keyed on the RESOLVED policy: reading props.removalPolicy here meant
|
|
37
|
+
// a key that resolved to DESTROY any other way got no window at all.
|
|
38
|
+
pendingWindow: removalPolicy === RemovalPolicy.DESTROY
|
|
39
|
+
? PENDING_DELETION_WINDOW
|
|
18
40
|
: undefined
|
|
19
41
|
});
|
|
20
42
|
new CfnOutput(this, `${outputName}KeyArn`, {
|
|
21
43
|
key: `${outputName}Arn`,
|
|
22
44
|
value: this.key.keyArn,
|
|
23
|
-
exportName: `${outputName}KeyArn`
|
|
45
|
+
exportName: stackScopedExportName(this, `${outputName}KeyArn`)
|
|
24
46
|
});
|
|
25
47
|
this.alias = new Alias(this, `${id}KeyAlias`, {
|
|
26
48
|
aliasName: props.aliasName || ResourceNaming.defaultKeyAlias(id),
|
|
@@ -29,7 +51,7 @@ export class CustomerManagedKey extends Construct {
|
|
|
29
51
|
new CfnOutput(this, `${outputName}KeyAliasArn`, {
|
|
30
52
|
key: `${outputName}AliasArn`,
|
|
31
53
|
value: this.alias.aliasArn,
|
|
32
|
-
exportName: `${outputName}KeyAliasArn`
|
|
54
|
+
exportName: stackScopedExportName(this, `${outputName}KeyAliasArn`)
|
|
33
55
|
});
|
|
34
56
|
}
|
|
35
57
|
}
|
|
@@ -26,7 +26,9 @@ export class SecureStringParameter extends Construct {
|
|
|
26
26
|
else {
|
|
27
27
|
this.cmk = new CustomerManagedKey(this, `${id}Key`, {
|
|
28
28
|
description: props.description || `${id} KMS Key`,
|
|
29
|
-
aliasName: props.aliasName
|
|
29
|
+
aliasName: props.aliasName,
|
|
30
|
+
// The parameter is deleted with the stack; nothing it encrypts survives.
|
|
31
|
+
protects: "stack-scoped"
|
|
30
32
|
});
|
|
31
33
|
}
|
|
32
34
|
this.parameter = new AwsCustomResource(this, `${id}SecureStringParameter`, {
|
|
@@ -72,7 +72,10 @@ export class Secret extends Construct {
|
|
|
72
72
|
}
|
|
73
73
|
// Create KMS key for new secrets only
|
|
74
74
|
const customerManagedKey = new CustomerManagedKey(this, `${id}CustomerManagedKey`, {
|
|
75
|
-
aliasName: props.aliasName ?? ResourceNaming.defaultKeyAlias(id)
|
|
75
|
+
aliasName: props.aliasName ?? ResourceNaming.defaultKeyAlias(id),
|
|
76
|
+
// The secret goes with the stack. Its 30-day recovery window is
|
|
77
|
+
// covered by the key's own pending-deletion window, not by retention.
|
|
78
|
+
protects: "stack-scoped"
|
|
76
79
|
});
|
|
77
80
|
this.secretsCustomerManagedKey = customerManagedKey;
|
|
78
81
|
/**
|
|
@@ -7,6 +7,12 @@ interface EcrProps {
|
|
|
7
7
|
/** Default IMMUTABLE (supply-chain guard). Dev-slot repos MUST pass MUTABLE —
|
|
8
8
|
* every `fjall dev up` repushes the branch tag (BUG-22). */
|
|
9
9
|
tagMutability?: TagMutability;
|
|
10
|
+
/**
|
|
11
|
+
* Overrides the env-aware default (D17: production → RETAIN, every other
|
|
12
|
+
* recognised environment → DESTROY). RETAIN outlives the stack; DESTROY
|
|
13
|
+
* takes the repository AND every image in it.
|
|
14
|
+
*/
|
|
15
|
+
removalPolicy?: "DESTROY" | "RETAIN";
|
|
10
16
|
}
|
|
11
17
|
export declare class EcrFactory {
|
|
12
18
|
static build(id: string, props?: EcrProps): (app: App, scope: Construct) => Ecr;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Repository, TagMutability } from "aws-cdk-lib/aws-ecr";
|
|
2
2
|
import { CfnOutput, RemovalPolicy } from "aws-cdk-lib";
|
|
3
|
+
import { envAwareRemovalPolicyDefault } from "../../../utils/removalPolicy.js";
|
|
3
4
|
export class EcrFactory {
|
|
4
5
|
static build(id, props) {
|
|
5
6
|
return (app, scope) => {
|
|
@@ -24,14 +25,17 @@ export class Ecr extends Repository {
|
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
27
|
static getRepositoryProps(props) {
|
|
27
|
-
|
|
28
|
-
// on stack teardown — next deploy pulls "not found" for any referenced tag.
|
|
28
|
+
const destroy = (props?.removalPolicy ?? envAwareRemovalPolicyDefault()) === "DESTROY";
|
|
29
29
|
return {
|
|
30
30
|
...(props?.repositoryName && { repositoryName: props.repositoryName }),
|
|
31
31
|
imageScanOnPush: true,
|
|
32
32
|
imageTagMutability: props?.tagMutability ?? TagMutability.IMMUTABLE,
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// CloudFormation refuses to delete a repository still holding images, so
|
|
34
|
+
// DESTROY with `false` is not a gentler DESTROY — it fails the stack
|
|
35
|
+
// delete outright. Gotcha: DESTROY wipes every pushed image, so a
|
|
36
|
+
// redeploy pulls "not found" for any tag the old repository held.
|
|
37
|
+
emptyOnDelete: destroy,
|
|
38
|
+
removalPolicy: destroy ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN
|
|
35
39
|
};
|
|
36
40
|
}
|
|
37
41
|
static build(id, props) {
|
|
@@ -36,6 +36,16 @@ export interface S3BucketProps extends BucketProps {
|
|
|
36
36
|
backupVaultTier?: BackupTier;
|
|
37
37
|
publicReadAccess?: boolean;
|
|
38
38
|
websiteHosting?: WebsiteHostingConfig;
|
|
39
|
+
/**
|
|
40
|
+
* Name a `fjallBucket(<name>)` alias target uses to import this bucket's
|
|
41
|
+
* website exports. Defaults to `bucketName ?? id`, which is right for a
|
|
42
|
+
* direct `new S3Bucket(scope, "Assets", …)` — `id` is then the author's own
|
|
43
|
+
* handle. A wrapper that nests the bucket under a derived id MUST pass this:
|
|
44
|
+
* `Storage` builds its bucket at `${id}Bucket`, so without it a
|
|
45
|
+
* `fjallBucket("assets")` record would import `AssetsWebsiteEndpoint`
|
|
46
|
+
* against an exported `AssetsBucketWebsiteEndpoint`.
|
|
47
|
+
*/
|
|
48
|
+
aliasTargetName?: string;
|
|
39
49
|
/**
|
|
40
50
|
* Declarative bucket-policy statements, each appended via
|
|
41
51
|
* `addToResourcePolicy`. The TLS-only `enforceSSL` deny is always applied
|
|
@@ -4,6 +4,7 @@ import { BlockPublicAccess, Bucket } from "aws-cdk-lib/aws-s3";
|
|
|
4
4
|
import { ArnPrincipal, Effect, PolicyStatement, StarPrincipal } from "aws-cdk-lib/aws-iam";
|
|
5
5
|
import { RegionInfo } from "aws-cdk-lib/region-info";
|
|
6
6
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
7
|
+
import { bucketWebsiteEndpointExportName, bucketWebsiteHostedZoneIdExportName } from "@fjall/util";
|
|
7
8
|
import { envAwareRemovalPolicyDefault, toRemovalPolicy } from "../../../utils/removalPolicy.js";
|
|
8
9
|
export { SDK_PRE_EMPTY_TAG_KEY } from "@fjall/util/aws";
|
|
9
10
|
function shouldAutoVersion(tier) {
|
|
@@ -17,7 +18,7 @@ function toResourcePolicyPrincipal(identifier) {
|
|
|
17
18
|
export class S3Bucket extends Bucket {
|
|
18
19
|
backupVaultTier;
|
|
19
20
|
constructor(scope, id, props = {}) {
|
|
20
|
-
const { websiteHosting, backupVaultTier, resourcePolicyStatements, ...cdkProps } = props;
|
|
21
|
+
const { websiteHosting, backupVaultTier, resourcePolicyStatements, aliasTargetName, ...cdkProps } = props;
|
|
21
22
|
const isPublic = props.publicReadAccess === true || websiteHosting !== undefined;
|
|
22
23
|
const versioned = props.versioned ?? shouldAutoVersion(backupVaultTier);
|
|
23
24
|
const removalPolicy = props.removalPolicy ?? toRemovalPolicy(envAwareRemovalPolicyDefault());
|
|
@@ -73,10 +74,14 @@ export class S3Bucket extends Bucket {
|
|
|
73
74
|
Tags.of(this).add(SDK_PRE_EMPTY_TAG_KEY, "true");
|
|
74
75
|
}
|
|
75
76
|
if (websiteHosting) {
|
|
76
|
-
const
|
|
77
|
+
const targetBucketName = aliasTargetName ?? props.bucketName ?? id;
|
|
78
|
+
// Only the export name routes through the contract helper. `safeBucket`
|
|
79
|
+
// stays local so the CfnOutput logical ids are unchanged — unifying the
|
|
80
|
+
// two would silently rename them.
|
|
81
|
+
const safeBucket = toPascalCase(targetBucketName.replace(/[^A-Za-z0-9-]/g, ""));
|
|
77
82
|
new CfnOutput(this, `${safeBucket}WebsiteEndpoint`, {
|
|
78
83
|
key: `${safeBucket}WebsiteEndpoint`,
|
|
79
|
-
exportName:
|
|
84
|
+
exportName: bucketWebsiteEndpointExportName(targetBucketName),
|
|
80
85
|
value: this.bucketWebsiteDomainName,
|
|
81
86
|
description: `S3 website endpoint for ${id} (consumed by Domain alias targets)`
|
|
82
87
|
});
|
|
@@ -88,7 +93,7 @@ export class S3Bucket extends Bucket {
|
|
|
88
93
|
}
|
|
89
94
|
new CfnOutput(this, `${safeBucket}WebsiteHostedZoneId`, {
|
|
90
95
|
key: `${safeBucket}WebsiteHostedZoneId`,
|
|
91
|
-
exportName:
|
|
96
|
+
exportName: bucketWebsiteHostedZoneIdExportName(targetBucketName),
|
|
92
97
|
value: websiteHostedZoneId,
|
|
93
98
|
description: `Route 53 hosted zone id for the S3 website endpoint of ${id}`
|
|
94
99
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Construct } from "constructs";
|
|
2
|
+
/**
|
|
3
|
+
* The app-level half of the `Domain` ALB alias-target contract.
|
|
4
|
+
*
|
|
5
|
+
* `fjallApp(appName)` resolves `<App>AlbDnsName` / `<App>AlbHostedZoneId` from
|
|
6
|
+
* a *different* stack, so the name has to be derivable from the app name
|
|
7
|
+
* alone — the Domain stack cannot see how many ALBs the app has. But an app
|
|
8
|
+
* may legitimately front several: one per ALB-bearing compute. Emitting the
|
|
9
|
+
* app-keyed pair from each of them produced the identical `Outputs` entry N
|
|
10
|
+
* times and CloudFormation rejected the entire template ("The Outputs section
|
|
11
|
+
* contains duplicate Export names"), which is how the compute deploy-test
|
|
12
|
+
* suite — seven ALB-bearing clusters in one app — failed on 2026-08-09.
|
|
13
|
+
*
|
|
14
|
+
* So the two halves are split by who can answer the question:
|
|
15
|
+
*
|
|
16
|
+
* - Each compute emits its own `<App>-<Compute>-Alb…` pair unconditionally.
|
|
17
|
+
* Always unique, always present, addressable as `fjallApp(app, compute)`.
|
|
18
|
+
* - The App emits the unqualified `<App>Alb…` pair at synth time, and ONLY
|
|
19
|
+
* when the app registered exactly one ALB — the sole case where "the
|
|
20
|
+
* app's ALB" names one thing.
|
|
21
|
+
*
|
|
22
|
+
* Two or more and no app-level export is written. A `Domain` using the bare
|
|
23
|
+
* `fjallApp(app)` then fails at CloudFormation with "No export named
|
|
24
|
+
* <App>AlbDnsName found", which names the missing export and is fixed by
|
|
25
|
+
* naming the compute. Picking one of N silently would deploy a domain
|
|
26
|
+
* pointing at an arbitrary load balancer, and would keep working right up
|
|
27
|
+
* until the registration order changed.
|
|
28
|
+
*/
|
|
29
|
+
export interface AlbAliasTargetRegistration {
|
|
30
|
+
readonly appName: string;
|
|
31
|
+
readonly computeName: string;
|
|
32
|
+
/** Stack-attached scope the app-level `CfnOutput` is emitted into. */
|
|
33
|
+
readonly scope: Construct;
|
|
34
|
+
readonly dnsName: string;
|
|
35
|
+
readonly hostedZoneId: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function registerAlbAliasTarget(registration: AlbAliasTargetRegistration): void;
|
|
38
|
+
/**
|
|
39
|
+
* Emit the unqualified app-level alias exports for every app in this tree
|
|
40
|
+
* that registered exactly one ALB. Call once, from `App.synth()`, before
|
|
41
|
+
* `super.synth()` — the construct tree is still mutable there, and that is
|
|
42
|
+
* the same window `applyTagsAspect()` uses.
|
|
43
|
+
*/
|
|
44
|
+
export declare function emitAppLevelAlbAliasExports(root: Construct): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { CfnOutput } from "aws-cdk-lib";
|
|
2
|
+
import { albDnsExportName, albHostedZoneIdExportName } from "@fjall/util";
|
|
3
|
+
import { toPascalCase } from "./capitaliseString.js";
|
|
4
|
+
import { FjallLogger } from "./validationLogger.js";
|
|
5
|
+
/**
|
|
6
|
+
* Keyed on the construct-tree root (the `App`) so two apps synthesised in one
|
|
7
|
+
* process — the platform and organisation stacks do this — never see each
|
|
8
|
+
* other's registrations. Weak so a discarded tree does not leak.
|
|
9
|
+
*/
|
|
10
|
+
const registryByRoot = new WeakMap();
|
|
11
|
+
export function registerAlbAliasTarget(registration) {
|
|
12
|
+
const root = registration.scope.node.root;
|
|
13
|
+
const existing = registryByRoot.get(root);
|
|
14
|
+
if (existing === undefined) {
|
|
15
|
+
registryByRoot.set(root, [registration]);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
existing.push(registration);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Emit the unqualified app-level alias exports for every app in this tree
|
|
22
|
+
* that registered exactly one ALB. Call once, from `App.synth()`, before
|
|
23
|
+
* `super.synth()` — the construct tree is still mutable there, and that is
|
|
24
|
+
* the same window `applyTagsAspect()` uses.
|
|
25
|
+
*/
|
|
26
|
+
export function emitAppLevelAlbAliasExports(root) {
|
|
27
|
+
const registrations = registryByRoot.get(root);
|
|
28
|
+
if (registrations === undefined)
|
|
29
|
+
return;
|
|
30
|
+
// `App.synth()` is re-entrant (CDK caches the assembly and returns it), and
|
|
31
|
+
// a second emission would collide on the output's construct id. Draining
|
|
32
|
+
// makes the call idempotent.
|
|
33
|
+
registryByRoot.delete(root);
|
|
34
|
+
const byApp = new Map();
|
|
35
|
+
for (const registration of registrations) {
|
|
36
|
+
const forApp = byApp.get(registration.appName);
|
|
37
|
+
if (forApp === undefined) {
|
|
38
|
+
byApp.set(registration.appName, [registration]);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
forApp.push(registration);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const [appName, forApp] of byApp) {
|
|
45
|
+
const only = forApp.length === 1 ? forApp[0] : undefined;
|
|
46
|
+
if (only === undefined) {
|
|
47
|
+
const computeNames = forApp.map((r) => r.computeName).join(", ");
|
|
48
|
+
FjallLogger.warn(`App '${appName}' fronts ${String(forApp.length)} load balancers (${computeNames}), ` +
|
|
49
|
+
`so no app-level '${albDnsExportName(appName)}' export was emitted — ` +
|
|
50
|
+
`'the app's ALB' would be ambiguous. Target a specific one with ` +
|
|
51
|
+
`fjallApp('${appName}', '<compute>') in your Domain records.`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Output logical ids are alphanumeric-only; the app name is not.
|
|
55
|
+
const logicalId = `${toPascalCase(appName)}AppAlb`;
|
|
56
|
+
new CfnOutput(only.scope, `${logicalId}DnsName`, {
|
|
57
|
+
key: `${logicalId}DnsName`,
|
|
58
|
+
exportName: albDnsExportName(appName),
|
|
59
|
+
value: only.dnsName,
|
|
60
|
+
description: `ALB DNS name for ${appName} (sole ALB; consumed by fjallApp('${appName}'))`
|
|
61
|
+
});
|
|
62
|
+
new CfnOutput(only.scope, `${logicalId}HostedZoneId`, {
|
|
63
|
+
key: `${logicalId}HostedZoneId`,
|
|
64
|
+
exportName: albHostedZoneIdExportName(appName),
|
|
65
|
+
value: only.hostedZoneId,
|
|
66
|
+
description: `ALB canonical hosted zone ID for ${appName} (sole ALB)`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/lib/utils/env.d.ts
CHANGED
|
@@ -47,6 +47,15 @@ export declare function getEnvironment(): string;
|
|
|
47
47
|
* sentinel meaning "no signal at all".
|
|
48
48
|
*/
|
|
49
49
|
export declare function hasExplicitEnvironmentSignal(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* True when this synth was driven by Fjall rather than run bare. deploy-core
|
|
52
|
+
* supplies `-c orgConfig=` on every synth it drives and nothing else does, so
|
|
53
|
+
* its presence separates "a developer is inspecting a template" from "an
|
|
54
|
+
* account is about to be deployed to" — a distinction consumers that pick a
|
|
55
|
+
* default on missing input need, because the safe default differs between the
|
|
56
|
+
* two. See `envAwareRemovalPolicyDefault`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isFjallDrivenSynth(): boolean;
|
|
50
59
|
/**
|
|
51
60
|
* Resolve an environment-specific value at CDK synth time.
|
|
52
61
|
*
|
package/dist/lib/utils/env.js
CHANGED
|
@@ -77,6 +77,18 @@ export function hasExplicitEnvironmentSignal() {
|
|
|
77
77
|
const ctxValue = parseContextArg("environment");
|
|
78
78
|
return ctxValue !== undefined && ctxValue !== "";
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* True when this synth was driven by Fjall rather than run bare. deploy-core
|
|
82
|
+
* supplies `-c orgConfig=` on every synth it drives and nothing else does, so
|
|
83
|
+
* its presence separates "a developer is inspecting a template" from "an
|
|
84
|
+
* account is about to be deployed to" — a distinction consumers that pick a
|
|
85
|
+
* default on missing input need, because the safe default differs between the
|
|
86
|
+
* two. See `envAwareRemovalPolicyDefault`.
|
|
87
|
+
*/
|
|
88
|
+
export function isFjallDrivenSynth() {
|
|
89
|
+
const raw = parseContextArg(CDK_CONTEXT_KEYS.ORG_CONFIG);
|
|
90
|
+
return raw !== undefined && raw !== "";
|
|
91
|
+
}
|
|
80
92
|
/**
|
|
81
93
|
* Resolve an environment-specific value at CDK synth time.
|
|
82
94
|
*
|