@fjall/components-infrastructure 2.13.0 → 2.15.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 +7 -7
- package/dist/lib/app.js +2 -3
- package/dist/lib/config/aws/accountMonitoringRole.js +2 -1
- package/dist/lib/config/aws/scpPreset.js +10 -1
- package/dist/lib/patterns/aws/storage.d.ts +1 -1
- package/dist/lib/patterns/aws/storage.js +5 -1
- package/dist/lib/resources/aws/logging/cloudTrail.d.ts +26 -4
- package/dist/lib/resources/aws/logging/cloudTrail.js +84 -11
- package/dist/lib/resources/aws/messaging/eventbridge.d.ts +3 -2
- package/dist/lib/resources/aws/messaging/eventbridge.js +2 -2
- package/dist/lib/resources/aws/networking/ipamPool.js +6 -3
- package/dist/lib/resources/aws/networking/serviceDiscovery.d.ts +4 -3
- package/dist/lib/resources/aws/networking/serviceDiscovery.js +2 -3
- package/dist/lib/resources/aws/storage/s3.d.ts +8 -0
- package/dist/lib/resources/aws/storage/s3.js +19 -4
- package/dist/lib/utils/env.d.ts +14 -0
- package/dist/lib/utils/env.js +21 -1
- package/dist/lib/utils/removalPolicy.d.ts +15 -0
- package/dist/lib/utils/removalPolicy.js +32 -0
- package/dist/lib/utils/standardTagsAspect.js +2 -1
- package/package.json +4 -4
package/dist/lib/app.d.ts
CHANGED
|
@@ -72,10 +72,11 @@ export interface IAppOptions {
|
|
|
72
72
|
*
|
|
73
73
|
* - `name?` overrides the AWS `EventBus.Name` (defaults to the app name).
|
|
74
74
|
* - `removalPolicy?` overrides the env-resolved default. Default resolves
|
|
75
|
-
* via `
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
75
|
+
* via `envAwareRemovalPolicyDefault()` per D17 (production → RETAIN,
|
|
76
|
+
* other recognised stages → DESTROY, unrecognised values fail synth) —
|
|
77
|
+
* NOT `process.env.NODE_ENV` (which is not set during CDK synth in
|
|
78
|
+
* Fjall's deployment paths). The shape is the normalised string union
|
|
79
|
+
* per D18(d), NOT the raw CDK `RemovalPolicy` enum.
|
|
79
80
|
*/
|
|
80
81
|
eventBus?: {
|
|
81
82
|
name?: string;
|
|
@@ -175,9 +176,8 @@ export declare class App extends CdkApp {
|
|
|
175
176
|
* Buses are recreatable in non-prod; the env-resolved default keeps prod
|
|
176
177
|
* history. The override (`App.getApp({ eventBus: { name?, removalPolicy? } })`)
|
|
177
178
|
* is consulted first; absent override falls back to the app name and the
|
|
178
|
-
* `
|
|
179
|
-
*
|
|
180
|
-
* deployment paths.
|
|
179
|
+
* `envAwareRemovalPolicyDefault()` resolution per D17. NODE_ENV is not
|
|
180
|
+
* consulted — it is not set during CDK synth in Fjall's deployment paths.
|
|
181
181
|
*/
|
|
182
182
|
getEventBus(): EventBusMessaging;
|
|
183
183
|
/**
|
package/dist/lib/app.js
CHANGED
|
@@ -277,9 +277,8 @@ export class App extends CdkApp {
|
|
|
277
277
|
* Buses are recreatable in non-prod; the env-resolved default keeps prod
|
|
278
278
|
* history. The override (`App.getApp({ eventBus: { name?, removalPolicy? } })`)
|
|
279
279
|
* is consulted first; absent override falls back to the app name and the
|
|
280
|
-
* `
|
|
281
|
-
*
|
|
282
|
-
* deployment paths.
|
|
280
|
+
* `envAwareRemovalPolicyDefault()` resolution per D17. NODE_ENV is not
|
|
281
|
+
* consulted — it is not set during CDK synth in Fjall's deployment paths.
|
|
283
282
|
*/
|
|
284
283
|
getEventBus() {
|
|
285
284
|
if (this.defaultEventBus) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ACCOUNT_MONITORING_ROLE_NAME } from "@fjall/util/aws";
|
|
1
2
|
import { CfnOutput } from "aws-cdk-lib";
|
|
2
3
|
import { Role, AccountPrincipal, PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
|
|
3
4
|
import { Construct } from "constructs";
|
|
@@ -16,7 +17,7 @@ export class AccountMonitoringRole extends Construct {
|
|
|
16
17
|
constructor(scope, id, props) {
|
|
17
18
|
super(scope, id);
|
|
18
19
|
this.role = new Role(this, "Role", {
|
|
19
|
-
roleName:
|
|
20
|
+
roleName: ACCOUNT_MONITORING_ROLE_NAME,
|
|
20
21
|
path: "/",
|
|
21
22
|
assumedBy: new AccountPrincipal(FJALL_PLATFORM_ACCOUNT_ID),
|
|
22
23
|
description: "Cross-account monitoring role for the Fjall platform. Grants read access to CloudWatch, ECS, RDS, S3, Lambda, ALB, Logs, and Cost Explorer.",
|
|
@@ -29,7 +29,16 @@ function buildFoundationGuardrails(allowedRegions) {
|
|
|
29
29
|
{
|
|
30
30
|
Sid: "DenyRootUserActions",
|
|
31
31
|
Effect: "Deny",
|
|
32
|
-
|
|
32
|
+
// Deny + NotAction denies everything except the policy-management
|
|
33
|
+
// scopes a root-task unlock session needs; the AWS root-task policies
|
|
34
|
+
// constrain those sessions further.
|
|
35
|
+
NotAction: [
|
|
36
|
+
"s3:GetBucketPolicy",
|
|
37
|
+
"s3:PutBucketPolicy",
|
|
38
|
+
"s3:DeleteBucketPolicy",
|
|
39
|
+
"sqs:GetQueueAttributes",
|
|
40
|
+
"sqs:SetQueueAttributes"
|
|
41
|
+
],
|
|
33
42
|
Resource: "*",
|
|
34
43
|
Condition: {
|
|
35
44
|
StringLike: {
|
|
@@ -38,7 +38,7 @@ export interface S3Props {
|
|
|
38
38
|
readonly backupVaultTier?: BackupTier;
|
|
39
39
|
readonly cors?: CorsRule[];
|
|
40
40
|
readonly deployment?: S3DeploymentConfig;
|
|
41
|
-
/** When true, sets RemovalPolicy.RETAIN
|
|
41
|
+
/** When true, sets RemovalPolicy.RETAIN (overriding the env-aware default). Used for imported buckets. */
|
|
42
42
|
readonly retain?: boolean;
|
|
43
43
|
}
|
|
44
44
|
export interface StorageBuildProps extends S3Props {
|
|
@@ -21,7 +21,11 @@ function toHttpMethod(method) {
|
|
|
21
21
|
DELETE: HttpMethods.DELETE,
|
|
22
22
|
HEAD: HttpMethods.HEAD
|
|
23
23
|
};
|
|
24
|
-
|
|
24
|
+
const mapped = methodMap[method];
|
|
25
|
+
if (mapped === undefined) {
|
|
26
|
+
throw new Error(`Unsupported CORS method "${method}" — expected one of: ${Object.keys(methodMap).join(", ")}.`);
|
|
27
|
+
}
|
|
28
|
+
return mapped;
|
|
25
29
|
}
|
|
26
30
|
function toCorsRules(cors) {
|
|
27
31
|
if (!cors)
|
|
@@ -6,10 +6,10 @@ import { S3Bucket } from "../storage/index.js";
|
|
|
6
6
|
interface CloudTrailProps extends CloudTrail.TrailProps {
|
|
7
7
|
bucketName: string;
|
|
8
8
|
/**
|
|
9
|
-
* Bucket + CMK survive stack deletion (RemovalPolicy.RETAIN
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Bucket + CMK survive stack deletion (RemovalPolicy.RETAIN). The trail
|
|
10
|
+
* resource itself stays DESTROY regardless — a retained trail keeps
|
|
11
|
+
* logging (and charging) as an unmanaged orphan. Default false preserves
|
|
12
|
+
* the historical TrailStack behaviour.
|
|
13
13
|
*/
|
|
14
14
|
retainAuditHistory?: boolean;
|
|
15
15
|
/**
|
|
@@ -25,6 +25,28 @@ export declare class Trail extends Construct {
|
|
|
25
25
|
readonly bucket: S3Bucket;
|
|
26
26
|
readonly encryptionKey: CustomerManagedKey;
|
|
27
27
|
constructor(scope: Construct, id: string, props: CloudTrailProps);
|
|
28
|
+
private trailArn;
|
|
29
|
+
/**
|
|
30
|
+
* Canonical CloudTrail delivery policy (ACL probe + log write), scoped to
|
|
31
|
+
* this trail's ARN via aws:SourceArn so no other trail — including one in
|
|
32
|
+
* another account — can deliver into or probe the bucket. Emitted from
|
|
33
|
+
* this construct rather than left to the L2 Trail because draining mode
|
|
34
|
+
* (omitTrail) skips the L2 construct while the trail being deleted may
|
|
35
|
+
* still deliver mid-update. Fjall-prefixed sids avoid colliding with the
|
|
36
|
+
* sid-less statements the L2 Trail adds when it is constructed.
|
|
37
|
+
*/
|
|
38
|
+
private addAccountTrailBucketPolicy;
|
|
39
|
+
/**
|
|
40
|
+
* Conditioned replacement for the KMS half of the retired grantReadWrite:
|
|
41
|
+
* `Bucket.grantReadWrite` silently granted the CloudTrail principal
|
|
42
|
+
* unconditioned Encrypt/Decrypt/ReEncrypt on the CMK because the bucket
|
|
43
|
+
* carries an encryptionKey. CloudTrail's actual needs are
|
|
44
|
+
* kms:GenerateDataKey* + kms:DescribeKey (trail-side log encryption,
|
|
45
|
+
* validated at CreateTrail) plus digest delivery via the bucket's default
|
|
46
|
+
* SSE-KMS — without these, trail creation fails with
|
|
47
|
+
* InsufficientEncryptionPolicyException.
|
|
48
|
+
*/
|
|
49
|
+
private addAccountTrailKeyPolicy;
|
|
28
50
|
/**
|
|
29
51
|
* The CDK L2 Trail contributes no KMS key-policy statements at all — it
|
|
30
52
|
* only sets kmsKeyId on the CfnTrail (verified in aws-cdk-lib 2.251.0).
|
|
@@ -41,6 +41,7 @@ export class Trail extends Construct {
|
|
|
41
41
|
removalPolicy: storagePolicy,
|
|
42
42
|
lifecycleRules: [{ expiration: Duration.days(365), enabled: true }]
|
|
43
43
|
});
|
|
44
|
+
const effectiveTrailName = props.trailName || `${id}Trail`;
|
|
44
45
|
if (props.isOrganizationTrail === true) {
|
|
45
46
|
// The L2 Trail emits the organisation-wide bucket policy itself
|
|
46
47
|
// (AWSLogs/<orgId>/* delivery); granting read/write here would shadow
|
|
@@ -48,7 +49,9 @@ export class Trail extends Construct {
|
|
|
48
49
|
this.addOrganisationTrailKeyPolicy(props, bucketName);
|
|
49
50
|
}
|
|
50
51
|
else {
|
|
51
|
-
this.
|
|
52
|
+
const accountTrailArn = this.trailArn(effectiveTrailName);
|
|
53
|
+
this.addAccountTrailBucketPolicy(accountTrailArn);
|
|
54
|
+
this.addAccountTrailKeyPolicy(accountTrailArn, bucketName);
|
|
52
55
|
}
|
|
53
56
|
if (omitTrail === true) {
|
|
54
57
|
return;
|
|
@@ -56,22 +59,92 @@ export class Trail extends Construct {
|
|
|
56
59
|
this.trail = new CloudTrail.Trail(this, `${id}CloudTrail`, {
|
|
57
60
|
...trailProps,
|
|
58
61
|
bucket: this.bucket,
|
|
59
|
-
trailName:
|
|
62
|
+
trailName: effectiveTrailName,
|
|
60
63
|
encryptionKey: this.encryptionKey.key
|
|
61
64
|
});
|
|
62
65
|
// Always DESTROY even when storage is retained: a RETAINED trail would
|
|
63
66
|
// keep logging (and charging) as an unmanaged orphan after stack
|
|
64
67
|
// deletion. Audit durability lives on the bucket + CMK removal policies.
|
|
65
68
|
this.trail.applyRemovalPolicy(RemovalPolicy.DESTROY);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
}
|
|
70
|
+
trailArn(trailName) {
|
|
71
|
+
const { partition, region, account } = Stack.of(this);
|
|
72
|
+
return `arn:${partition}:cloudtrail:${region}:${account}:trail/${trailName}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Canonical CloudTrail delivery policy (ACL probe + log write), scoped to
|
|
76
|
+
* this trail's ARN via aws:SourceArn so no other trail — including one in
|
|
77
|
+
* another account — can deliver into or probe the bucket. Emitted from
|
|
78
|
+
* this construct rather than left to the L2 Trail because draining mode
|
|
79
|
+
* (omitTrail) skips the L2 construct while the trail being deleted may
|
|
80
|
+
* still deliver mid-update. Fjall-prefixed sids avoid colliding with the
|
|
81
|
+
* sid-less statements the L2 Trail adds when it is constructed.
|
|
82
|
+
*/
|
|
83
|
+
addAccountTrailBucketPolicy(trailArn) {
|
|
84
|
+
const { account } = Stack.of(this);
|
|
85
|
+
const cloudTrailPrincipal = new ServicePrincipal("cloudtrail.amazonaws.com");
|
|
86
|
+
this.bucket.addToResourcePolicy(new PolicyStatement({
|
|
87
|
+
sid: "FjallCloudTrailAclCheck",
|
|
88
|
+
principals: [cloudTrailPrincipal],
|
|
89
|
+
actions: ["s3:GetBucketAcl"],
|
|
90
|
+
resources: [this.bucket.bucketArn],
|
|
91
|
+
conditions: {
|
|
92
|
+
StringEquals: { "aws:SourceArn": trailArn }
|
|
93
|
+
}
|
|
94
|
+
}));
|
|
95
|
+
this.bucket.addToResourcePolicy(new PolicyStatement({
|
|
96
|
+
sid: "FjallCloudTrailWrite",
|
|
97
|
+
principals: [cloudTrailPrincipal],
|
|
98
|
+
actions: ["s3:PutObject"],
|
|
99
|
+
resources: [this.bucket.arnForObjects(`AWSLogs/${account}/*`)],
|
|
100
|
+
conditions: {
|
|
101
|
+
StringEquals: {
|
|
102
|
+
"aws:SourceArn": trailArn,
|
|
103
|
+
"s3:x-amz-acl": "bucket-owner-full-control"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Conditioned replacement for the KMS half of the retired grantReadWrite:
|
|
110
|
+
* `Bucket.grantReadWrite` silently granted the CloudTrail principal
|
|
111
|
+
* unconditioned Encrypt/Decrypt/ReEncrypt on the CMK because the bucket
|
|
112
|
+
* carries an encryptionKey. CloudTrail's actual needs are
|
|
113
|
+
* kms:GenerateDataKey* + kms:DescribeKey (trail-side log encryption,
|
|
114
|
+
* validated at CreateTrail) plus digest delivery via the bucket's default
|
|
115
|
+
* SSE-KMS — without these, trail creation fails with
|
|
116
|
+
* InsufficientEncryptionPolicyException.
|
|
117
|
+
*/
|
|
118
|
+
addAccountTrailKeyPolicy(trailArn, bucketName) {
|
|
119
|
+
const { partition } = Stack.of(this);
|
|
120
|
+
const cloudTrailPrincipal = new ServicePrincipal("cloudtrail.amazonaws.com");
|
|
121
|
+
this.encryptionKey.key.addToResourcePolicy(new PolicyStatement({
|
|
122
|
+
sid: "FjallCloudTrailKeyUse",
|
|
123
|
+
principals: [cloudTrailPrincipal],
|
|
124
|
+
actions: ["kms:GenerateDataKey*", "kms:DescribeKey"],
|
|
125
|
+
resources: ["*"],
|
|
126
|
+
conditions: {
|
|
127
|
+
StringEquals: { "aws:SourceArn": trailArn }
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
130
|
+
this.encryptionKey.key.addToResourcePolicy(new PolicyStatement({
|
|
131
|
+
sid: "FjallCloudTrailDigestDelivery",
|
|
132
|
+
principals: [cloudTrailPrincipal],
|
|
133
|
+
actions: ["kms:GenerateDataKey*", "kms:Decrypt"],
|
|
134
|
+
resources: ["*"],
|
|
135
|
+
conditions: {
|
|
136
|
+
// Digest files arrive via the bucket's default SSE-KMS. Two exact
|
|
137
|
+
// entries (bucket-level context + per-object contexts) — a bare
|
|
138
|
+
// `${bucketName}*` would also match sibling bucket NAMES sharing
|
|
139
|
+
// the prefix.
|
|
140
|
+
StringLike: {
|
|
141
|
+
"kms:EncryptionContext:aws:s3:arn": [
|
|
142
|
+
`arn:${partition}:s3:::${bucketName}`,
|
|
143
|
+
`arn:${partition}:s3:::${bucketName}/*`
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}));
|
|
75
148
|
}
|
|
76
149
|
/**
|
|
77
150
|
* The CDK L2 Trail contributes no KMS key-policy statements at all — it
|
|
@@ -14,8 +14,9 @@ export interface EventBridgeBusProps {
|
|
|
14
14
|
/** Override the default description ("EventBus <appName> — Fjall app event bus"). */
|
|
15
15
|
description?: string;
|
|
16
16
|
/**
|
|
17
|
-
* Removal policy. Default resolves via
|
|
18
|
-
* RETAIN;
|
|
17
|
+
* Removal policy. Default resolves via `envAwareRemovalPolicyDefault()`
|
|
18
|
+
* (production → RETAIN; other recognised stages → DESTROY; unrecognised
|
|
19
|
+
* values fail synth) — D17 explicitly rejects `process.env.NODE_ENV`
|
|
19
20
|
* because it is not set during CDK synth in Fjall's deployment paths.
|
|
20
21
|
* Buses are recreatable in non-prod; production keeps history.
|
|
21
22
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { CfnOutput } from "aws-cdk-lib";
|
|
3
3
|
import { EventBus } from "aws-cdk-lib/aws-events";
|
|
4
|
-
import {
|
|
4
|
+
import { envAwareRemovalPolicyDefault } from "../../../utils/removalPolicy.js";
|
|
5
5
|
import { toRemovalPolicy } from "./utils.js";
|
|
6
6
|
export class EventBridgeBus extends Construct {
|
|
7
7
|
id;
|
|
@@ -17,7 +17,7 @@ export class EventBridgeBus extends Construct {
|
|
|
17
17
|
(props.appName
|
|
18
18
|
? `EventBus ${props.appName} — Fjall app event bus`
|
|
19
19
|
: undefined);
|
|
20
|
-
const removalPolicyValue = props.removalPolicy ??
|
|
20
|
+
const removalPolicyValue = props.removalPolicy ?? envAwareRemovalPolicyDefault();
|
|
21
21
|
const ownedBus = new EventBus(this, `${id}EventBus`, {
|
|
22
22
|
eventBusName: props.eventBusName,
|
|
23
23
|
description
|
|
@@ -8,8 +8,11 @@ import { CustomResource } from "../utilities/customResource.js";
|
|
|
8
8
|
import getAccountId from "../../../utils/getAccountId.js";
|
|
9
9
|
import { accountConstructKey, findAccountNameCollision } from "../../../utils/capitaliseString.js";
|
|
10
10
|
import { FjallLogger } from "../../../utils/validationLogger.js";
|
|
11
|
+
import { formatIpamPairTagValue, IPAM_OPERATIONS_POOL_TAG_KEY } from "@fjall/util/aws";
|
|
11
12
|
const IPAM_TAGS = {
|
|
12
|
-
|
|
13
|
+
// Shared with the deploy-core readiness probe, which matches pools by this
|
|
14
|
+
// key + the pair value format — see @fjall/util/aws ipamTags.
|
|
15
|
+
OPERATIONS_POOL: IPAM_OPERATIONS_POOL_TAG_KEY,
|
|
13
16
|
COST_ALLOCATION_ENVIRONMENT: "fjall:costAllocation:environment",
|
|
14
17
|
COST_ALLOCATION_ACCOUNT_NAME: "fjall:costAllocation:accountName"
|
|
15
18
|
};
|
|
@@ -134,14 +137,14 @@ export class IpamPool extends Construct {
|
|
|
134
137
|
allocationResourceTags: [
|
|
135
138
|
{
|
|
136
139
|
key: IPAM_TAGS.OPERATIONS_POOL,
|
|
137
|
-
value:
|
|
140
|
+
value: formatIpamPairTagValue(accountId, region)
|
|
138
141
|
}
|
|
139
142
|
],
|
|
140
143
|
autoImport: true,
|
|
141
144
|
tags: [
|
|
142
145
|
{
|
|
143
146
|
key: IPAM_TAGS.OPERATIONS_POOL,
|
|
144
|
-
value:
|
|
147
|
+
value: formatIpamPairTagValue(accountId, region)
|
|
145
148
|
},
|
|
146
149
|
{
|
|
147
150
|
key: IPAM_TAGS.COST_ALLOCATION_ACCOUNT_NAME,
|
|
@@ -18,9 +18,10 @@ export interface ServiceDiscoveryNamespaceProps {
|
|
|
18
18
|
/** Override the default description (`"ServiceDiscovery <name> — Fjall private DNS namespace"`). */
|
|
19
19
|
description?: string;
|
|
20
20
|
/**
|
|
21
|
-
* Removal policy. Default resolves via
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* Removal policy. Default resolves via `envAwareRemovalPolicyDefault()`
|
|
22
|
+
* (production → RETAIN; other recognised stages → DESTROY; unrecognised
|
|
23
|
+
* values fail synth) — matches the convention codified in D17 of the
|
|
24
|
+
* EventBridge promotion design. NODE_ENV is intentionally NOT consulted
|
|
24
25
|
* because it is unset during CDK synth in Fjall's deployment paths.
|
|
25
26
|
*/
|
|
26
27
|
removalPolicy?: "DESTROY" | "RETAIN";
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { CfnOutput, Duration } from "aws-cdk-lib";
|
|
3
3
|
import { PrivateDnsNamespace, DnsRecordType } from "aws-cdk-lib/aws-servicediscovery";
|
|
4
|
-
import {
|
|
5
|
-
import { toRemovalPolicy } from "../../../utils/removalPolicy.js";
|
|
4
|
+
import { envAwareRemovalPolicyDefault, toRemovalPolicy } from "../../../utils/removalPolicy.js";
|
|
6
5
|
export class ServiceDiscoveryNamespace extends Construct {
|
|
7
6
|
id;
|
|
8
7
|
#namespace;
|
|
@@ -12,7 +11,7 @@ export class ServiceDiscoveryNamespace extends Construct {
|
|
|
12
11
|
this.id = id;
|
|
13
12
|
const description = props.description ??
|
|
14
13
|
`ServiceDiscovery ${props.name} — Fjall private DNS namespace`;
|
|
15
|
-
const removalPolicyValue = props.removalPolicy ??
|
|
14
|
+
const removalPolicyValue = props.removalPolicy ?? envAwareRemovalPolicyDefault();
|
|
16
15
|
const namespace = new PrivateDnsNamespace(this, `${id}Namespace`, {
|
|
17
16
|
vpc: props.vpc,
|
|
18
17
|
name: props.name,
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { Bucket, type BucketProps } from "aws-cdk-lib/aws-s3";
|
|
2
2
|
import { type Construct } from "constructs";
|
|
3
3
|
import { type BackupTier } from "../../../utils/backupTierMapping.js";
|
|
4
|
+
export { SDK_PRE_EMPTY_TAG_KEY } from "@fjall/util/aws";
|
|
4
5
|
export interface WebsiteHostingConfig {
|
|
5
6
|
readonly indexDocument: string;
|
|
6
7
|
readonly errorDocument?: string;
|
|
7
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Props for {@link S3Bucket}.
|
|
11
|
+
*
|
|
12
|
+
* `autoDeleteObjects` is accepted for source compatibility but always
|
|
13
|
+
* overridden to `false` (ADR D4.3) — passing `true` raises a synth-time
|
|
14
|
+
* warning rather than a compile error so older scaffolds keep building.
|
|
15
|
+
*/
|
|
8
16
|
export interface S3BucketProps extends BucketProps {
|
|
9
17
|
backupVaultTier?: BackupTier;
|
|
10
18
|
publicReadAccess?: boolean;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SDK_PRE_EMPTY_TAG_KEY } from "@fjall/util/aws";
|
|
2
|
+
import { Annotations, CfnOutput, Duration, RemovalPolicy, Tags } from "aws-cdk-lib";
|
|
2
3
|
import { BlockPublicAccess, Bucket } from "aws-cdk-lib/aws-s3";
|
|
3
4
|
import { RegionInfo } from "aws-cdk-lib/region-info";
|
|
4
5
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
6
|
+
import { envAwareRemovalPolicyDefault, toRemovalPolicy } from "../../../utils/removalPolicy.js";
|
|
7
|
+
export { SDK_PRE_EMPTY_TAG_KEY } from "@fjall/util/aws";
|
|
5
8
|
function shouldAutoVersion(tier) {
|
|
6
9
|
return tier === "resilient" || tier === "enterprise";
|
|
7
10
|
}
|
|
@@ -10,13 +13,16 @@ export class S3Bucket extends Bucket {
|
|
|
10
13
|
constructor(scope, id, props = {}) {
|
|
11
14
|
const { websiteHosting, backupVaultTier, ...cdkProps } = props;
|
|
12
15
|
const isPublic = props.publicReadAccess === true || websiteHosting !== undefined;
|
|
13
|
-
const isRetained = props.removalPolicy === RemovalPolicy.RETAIN;
|
|
14
16
|
const versioned = props.versioned ?? shouldAutoVersion(backupVaultTier);
|
|
17
|
+
const removalPolicy = props.removalPolicy ?? toRemovalPolicy(envAwareRemovalPolicyDefault());
|
|
15
18
|
super(scope, id, {
|
|
16
19
|
...cdkProps,
|
|
17
20
|
enforceSSL: true,
|
|
18
|
-
autoDeleteObjects:
|
|
19
|
-
|
|
21
|
+
// autoDeleteObjects retired (ADR D4.3): the CDK custom resource's deny
|
|
22
|
+
// policy is the quarantine mechanism — destroy-time emptying is done
|
|
23
|
+
// SDK-side by deploy-core's pre-empty pass, keyed off the marker tag.
|
|
24
|
+
autoDeleteObjects: false,
|
|
25
|
+
removalPolicy,
|
|
20
26
|
publicReadAccess: isPublic,
|
|
21
27
|
...(isPublic && {
|
|
22
28
|
blockPublicAccess: new BlockPublicAccess({
|
|
@@ -36,6 +42,15 @@ export class S3Bucket extends Bucket {
|
|
|
36
42
|
: props.lifecycleRules
|
|
37
43
|
});
|
|
38
44
|
this.backupVaultTier = backupVaultTier;
|
|
45
|
+
if (props.autoDeleteObjects === true) {
|
|
46
|
+
Annotations.of(this).addWarningV2("@fjall/components-infrastructure:s3:autoDeleteObjectsIgnored", "autoDeleteObjects: true is ignored — the CDK auto-delete custom " +
|
|
47
|
+
"resource is retired (ADR D4.3). DESTROY buckets are emptied " +
|
|
48
|
+
"SDK-side by deploy-core's pre-empty pass via the " +
|
|
49
|
+
`${SDK_PRE_EMPTY_TAG_KEY} marker tag. Remove the prop.`);
|
|
50
|
+
}
|
|
51
|
+
if (removalPolicy === RemovalPolicy.DESTROY) {
|
|
52
|
+
Tags.of(this).add(SDK_PRE_EMPTY_TAG_KEY, "true");
|
|
53
|
+
}
|
|
39
54
|
if (websiteHosting) {
|
|
40
55
|
const safeBucket = toPascalCase((props.bucketName ?? id).replace(/[^A-Za-z0-9-]/g, ""));
|
|
41
56
|
new CfnOutput(this, `${safeBucket}WebsiteEndpoint`, {
|
package/dist/lib/utils/env.d.ts
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
type EnvConfig<T> = {
|
|
17
17
|
default: T;
|
|
18
18
|
} & Partial<Record<string, T>>;
|
|
19
|
+
/**
|
|
20
|
+
* Sentinel returned by `getEnvironment()` when no environment signal exists.
|
|
21
|
+
* Consumers that need to distinguish "no signal" from "unrecognised value"
|
|
22
|
+
* (e.g. `envAwareRemovalPolicyDefault()`) compare against this.
|
|
23
|
+
*/
|
|
24
|
+
export declare const UNKNOWN_ENVIRONMENT = "unknown";
|
|
19
25
|
/**
|
|
20
26
|
* Resolve the current deployment environment without App dependency.
|
|
21
27
|
*
|
|
@@ -33,6 +39,14 @@ type EnvConfig<T> = {
|
|
|
33
39
|
* ENVIRONMENT as the dominant ambient signal.
|
|
34
40
|
*/
|
|
35
41
|
export declare function getEnvironment(): string;
|
|
42
|
+
/**
|
|
43
|
+
* True when the environment came from an explicit operator signal
|
|
44
|
+
* (`ENVIRONMENT` env var or `-c environment=`) rather than account-lookup
|
|
45
|
+
* inference or the no-signal fallback. Lets consumers distinguish an
|
|
46
|
+
* explicitly-supplied literal "unknown" from the `UNKNOWN_ENVIRONMENT`
|
|
47
|
+
* sentinel meaning "no signal at all".
|
|
48
|
+
*/
|
|
49
|
+
export declare function hasExplicitEnvironmentSignal(): boolean;
|
|
36
50
|
/**
|
|
37
51
|
* Resolve an environment-specific value at CDK synth time.
|
|
38
52
|
*
|
package/dist/lib/utils/env.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { CDK_CONTEXT_KEYS } from "./cdkContext.js";
|
|
2
2
|
import { parseOrgConfig, resolveSynthEnvironment } from "./orgConfigParser.js";
|
|
3
3
|
import { FjallLogger } from "./validationLogger.js";
|
|
4
|
+
/**
|
|
5
|
+
* Sentinel returned by `getEnvironment()` when no environment signal exists.
|
|
6
|
+
* Consumers that need to distinguish "no signal" from "unrecognised value"
|
|
7
|
+
* (e.g. `envAwareRemovalPolicyDefault()`) compare against this.
|
|
8
|
+
*/
|
|
9
|
+
export const UNKNOWN_ENVIRONMENT = "unknown";
|
|
4
10
|
let cachedEnvironment;
|
|
5
11
|
/**
|
|
6
12
|
* Resolve the current deployment environment without App dependency.
|
|
@@ -54,9 +60,23 @@ export function getEnvironment() {
|
|
|
54
60
|
// 5. Fallback
|
|
55
61
|
FjallLogger.warn("[fjall] Could not determine environment. " +
|
|
56
62
|
"Set ENVIRONMENT env var or pass -c environment=<value>.");
|
|
57
|
-
cachedEnvironment =
|
|
63
|
+
cachedEnvironment = UNKNOWN_ENVIRONMENT;
|
|
58
64
|
return cachedEnvironment;
|
|
59
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* True when the environment came from an explicit operator signal
|
|
68
|
+
* (`ENVIRONMENT` env var or `-c environment=`) rather than account-lookup
|
|
69
|
+
* inference or the no-signal fallback. Lets consumers distinguish an
|
|
70
|
+
* explicitly-supplied literal "unknown" from the `UNKNOWN_ENVIRONMENT`
|
|
71
|
+
* sentinel meaning "no signal at all".
|
|
72
|
+
*/
|
|
73
|
+
export function hasExplicitEnvironmentSignal() {
|
|
74
|
+
const envVar = process.env.ENVIRONMENT;
|
|
75
|
+
if (envVar !== undefined && envVar !== "")
|
|
76
|
+
return true;
|
|
77
|
+
const ctxValue = parseContextArg("environment");
|
|
78
|
+
return ctxValue !== undefined && ctxValue !== "";
|
|
79
|
+
}
|
|
60
80
|
/**
|
|
61
81
|
* Resolve an environment-specific value at CDK synth time.
|
|
62
82
|
*
|
|
@@ -1,2 +1,17 @@
|
|
|
1
1
|
import { RemovalPolicy } from "aws-cdk-lib";
|
|
2
2
|
export declare function toRemovalPolicy(value?: "DESTROY" | "RETAIN" | "SNAPSHOT"): RemovalPolicy;
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the env-aware removal-policy default (D17): production → RETAIN,
|
|
5
|
+
* every other recognised environment → DESTROY.
|
|
6
|
+
*
|
|
7
|
+
* Unlike the generic `env()` resolver (where unrecognised → default is
|
|
8
|
+
* benign), an unrecognised value here throws at synth: silently landing a
|
|
9
|
+
* typo like `ENVIRONMENT=prod` on DESTROY deletes data when the stack is
|
|
10
|
+
* deleted. The accept-set derives from `ACCOUNT_STAGES_WITH_ROOT` so a new
|
|
11
|
+
* stage added in `@fjall/util` widens it automatically. The no-signal
|
|
12
|
+
* sentinel keeps the historical warn-and-DESTROY behaviour — raw `cdk synth`
|
|
13
|
+
* without context and null-stage cascade synths rely on it. An explicitly
|
|
14
|
+
* supplied `ENVIRONMENT=unknown` collides with the sentinel string and would
|
|
15
|
+
* otherwise ride the silent-DESTROY path, so it is treated as unrecognised.
|
|
16
|
+
*/
|
|
17
|
+
export declare function envAwareRemovalPolicyDefault(): "DESTROY" | "RETAIN";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { ACCOUNT_STAGES_WITH_ROOT } from "@fjall/util";
|
|
1
2
|
import { RemovalPolicy } from "aws-cdk-lib";
|
|
3
|
+
import { getEnvironment, hasExplicitEnvironmentSignal, UNKNOWN_ENVIRONMENT } from "./env.js";
|
|
2
4
|
export function toRemovalPolicy(value) {
|
|
3
5
|
switch (value) {
|
|
4
6
|
case "DESTROY":
|
|
@@ -10,3 +12,33 @@ export function toRemovalPolicy(value) {
|
|
|
10
12
|
return RemovalPolicy.RETAIN;
|
|
11
13
|
}
|
|
12
14
|
}
|
|
15
|
+
const REMOVAL_DEFAULT_ENVIRONMENTS = new Set(ACCOUNT_STAGES_WITH_ROOT);
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the env-aware removal-policy default (D17): production → RETAIN,
|
|
18
|
+
* every other recognised environment → DESTROY.
|
|
19
|
+
*
|
|
20
|
+
* Unlike the generic `env()` resolver (where unrecognised → default is
|
|
21
|
+
* benign), an unrecognised value here throws at synth: silently landing a
|
|
22
|
+
* typo like `ENVIRONMENT=prod` on DESTROY deletes data when the stack is
|
|
23
|
+
* deleted. The accept-set derives from `ACCOUNT_STAGES_WITH_ROOT` so a new
|
|
24
|
+
* stage added in `@fjall/util` widens it automatically. The no-signal
|
|
25
|
+
* sentinel keeps the historical warn-and-DESTROY behaviour — raw `cdk synth`
|
|
26
|
+
* without context and null-stage cascade synths rely on it. An explicitly
|
|
27
|
+
* supplied `ENVIRONMENT=unknown` collides with the sentinel string and would
|
|
28
|
+
* otherwise ride the silent-DESTROY path, so it is treated as unrecognised.
|
|
29
|
+
*/
|
|
30
|
+
export function envAwareRemovalPolicyDefault() {
|
|
31
|
+
const environment = getEnvironment();
|
|
32
|
+
if (environment === UNKNOWN_ENVIRONMENT && !hasExplicitEnvironmentSignal()) {
|
|
33
|
+
return "DESTROY";
|
|
34
|
+
}
|
|
35
|
+
if (!REMOVAL_DEFAULT_ENVIRONMENTS.has(environment)) {
|
|
36
|
+
throw new Error(`Unrecognised environment "${environment}" — refusing to resolve the ` +
|
|
37
|
+
`env-aware removal-policy default (non-production environments ` +
|
|
38
|
+
`default to DESTROY, which deletes data when the stack is deleted). ` +
|
|
39
|
+
`Valid values: ${ACCOUNT_STAGES_WITH_ROOT.join(", ")}. ` +
|
|
40
|
+
`Set ENVIRONMENT or pass -c environment=<value>, or set an explicit ` +
|
|
41
|
+
`removalPolicy on the construct.`);
|
|
42
|
+
}
|
|
43
|
+
return environment === "production" ? "RETAIN" : "DESTROY";
|
|
44
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CfnResource, Stack, Tags } from "aws-cdk-lib";
|
|
2
2
|
import { BACKUP_TIER_TAG_KEY, BACKUP_TIER_TAG_MAP } from "./backupTierMapping.js";
|
|
3
|
+
import { formatIpamPairTagValue, IPAM_OPERATIONS_POOL_TAG_KEY } from "@fjall/util/aws";
|
|
3
4
|
/**
|
|
4
5
|
* Aspect to apply special Fjall tags to specific resource types.
|
|
5
6
|
*
|
|
@@ -49,7 +50,7 @@ export class StandardTagsAspect {
|
|
|
49
50
|
process.env.CDK_DEFAULT_ACCOUNT;
|
|
50
51
|
const region = stack.region || process.env.CDK_DEFAULT_REGION;
|
|
51
52
|
if (accountId && region) {
|
|
52
|
-
Tags.of(vpc).add(
|
|
53
|
+
Tags.of(vpc).add(IPAM_OPERATIONS_POOL_TAG_KEY, formatIpamPairTagValue(accountId, region));
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
66
|
-
"@fjall/generator": "^2.
|
|
67
|
-
"@fjall/util": "^2.
|
|
66
|
+
"@fjall/generator": "^2.15.0",
|
|
67
|
+
"@fjall/util": "^2.15.0",
|
|
68
68
|
"constructs": "^10.0.0",
|
|
69
69
|
"uuid": "^14.0.0"
|
|
70
70
|
},
|
|
@@ -79,5 +79,5 @@
|
|
|
79
79
|
"engines": {
|
|
80
80
|
"node": ">=18.0.0"
|
|
81
81
|
},
|
|
82
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "b2223855907cb6d467e47df073b2a5b28c684ede"
|
|
83
83
|
}
|