@fjall/components-infrastructure 37.0.0 → 39.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 +52 -12
- package/dist/lib/app.js +60 -24
- package/dist/lib/config/aws/configRecorder.js +3 -0
- package/dist/lib/config/aws/disasterRecovery.d.ts +1 -2
- package/dist/lib/config/aws/disasterRecovery.js +167 -45
- package/dist/lib/patterns/aws/buildkite/buildkite.js +7 -2
- package/dist/lib/patterns/aws/cdn.d.ts +19 -11
- package/dist/lib/patterns/aws/cdn.js +53 -25
- package/dist/lib/patterns/aws/clickhouseDatabase.js +8 -4
- package/dist/lib/patterns/aws/computeEcs.js +17 -1
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +30 -4
- package/dist/lib/patterns/aws/computePropApplicability.js +1 -0
- package/dist/lib/patterns/aws/domain.js +3 -3
- package/dist/lib/patterns/aws/externalRecordsPattern.d.ts +9 -6
- package/dist/lib/patterns/aws/externalRecordsPattern.js +22 -46
- package/dist/lib/patterns/aws/interfaces/domain.d.ts +8 -5
- package/dist/lib/patterns/aws/interfaces/pattern.d.ts +9 -10
- package/dist/lib/patterns/aws/messaging.d.ts +10 -0
- package/dist/lib/patterns/aws/messaging.js +10 -6
- package/dist/lib/patterns/aws/payload.d.ts +5 -0
- package/dist/lib/patterns/aws/payload.js +23 -11
- package/dist/lib/patterns/aws/storage.d.ts +41 -0
- package/dist/lib/patterns/aws/storage.js +23 -8
- package/dist/lib/patterns/aws/targets/targetResolution.d.ts +4 -4
- package/dist/lib/patterns/aws/targets/targetResolution.js +4 -4
- package/dist/lib/resources/aws/backup/backupVault.d.ts +27 -2
- package/dist/lib/resources/aws/backup/backupVault.js +49 -6
- package/dist/lib/resources/aws/cdn/cloudFront.d.ts +42 -18
- package/dist/lib/resources/aws/cdn/cloudFront.js +39 -51
- package/dist/lib/resources/aws/cdn/cloudFrontLogDelivery.d.ts +90 -0
- package/dist/lib/resources/aws/cdn/cloudFrontLogDelivery.js +213 -0
- package/dist/lib/resources/aws/cdn/index.d.ts +1 -0
- package/dist/lib/resources/aws/cdn/index.js +1 -0
- package/dist/lib/resources/aws/compute/ecs.d.ts +7 -4
- package/dist/lib/resources/aws/compute/ecs.js +27 -21
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +3 -0
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +11 -0
- package/dist/lib/resources/aws/compute/ecsValidation.d.ts +8 -0
- package/dist/lib/resources/aws/compute/ecsValidation.js +8 -0
- package/dist/lib/resources/aws/compute/persistentDataVolume.d.ts +8 -0
- package/dist/lib/resources/aws/compute/persistentDataVolume.js +2 -0
- package/dist/lib/resources/aws/database/dynamodb.js +2 -1
- package/dist/lib/resources/aws/database/rdsAurora.js +6 -1
- package/dist/lib/resources/aws/database/rdsInstance.js +6 -1
- package/dist/lib/resources/aws/logging/accessLogBucket.d.ts +15 -8
- package/dist/lib/resources/aws/logging/accessLogBucket.js +16 -9
- package/dist/lib/resources/aws/logging/cloudTrail.js +3 -0
- package/dist/lib/resources/aws/messaging/eventBridgeQueueGrants.d.ts +16 -0
- package/dist/lib/resources/aws/messaging/eventBridgeQueueGrants.js +91 -0
- package/dist/lib/resources/aws/messaging/eventBridgeRule.js +36 -5
- package/dist/lib/resources/aws/messaging/eventTargets.d.ts +4 -3
- package/dist/lib/resources/aws/messaging/eventTargets.js +71 -31
- package/dist/lib/resources/aws/messaging/queuePolicyQuota.d.ts +25 -0
- package/dist/lib/resources/aws/messaging/queuePolicyQuota.js +190 -0
- package/dist/lib/resources/aws/messaging/schedule.d.ts +5 -5
- package/dist/lib/resources/aws/messaging/sqs.d.ts +36 -1
- package/dist/lib/resources/aws/messaging/sqs.js +40 -2
- package/dist/lib/resources/aws/messaging/subscription.d.ts +4 -3
- package/dist/lib/resources/aws/networking/vpc.js +12 -6
- package/dist/lib/resources/aws/secrets/stackEncryptionKey.d.ts +26 -1
- package/dist/lib/resources/aws/secrets/stackEncryptionKey.js +56 -1
- package/dist/lib/resources/aws/storage/s3.d.ts +21 -1
- package/dist/lib/resources/aws/storage/s3.js +89 -3
- package/dist/lib/utils/backupTierMapping.d.ts +23 -2
- package/dist/lib/utils/backupTierMapping.js +25 -0
- package/dist/lib/utils/cdkContext.d.ts +14 -0
- package/dist/lib/utils/cdkContext.js +38 -1
- package/dist/lib/utils/exportNaming.d.ts +30 -27
- package/dist/lib/utils/exportNaming.js +32 -28
- package/dist/lib/utils/standardTagsAspect.d.ts +8 -18
- package/dist/lib/utils/standardTagsAspect.js +8 -34
- package/package.json +4 -4
package/dist/lib/app.d.ts
CHANGED
|
@@ -54,10 +54,15 @@ export interface IAppOptions {
|
|
|
54
54
|
};
|
|
55
55
|
/**
|
|
56
56
|
* Backup configuration for automatic AWS Backup enrolment.
|
|
57
|
-
* - Object with tier:
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
57
|
+
* - Object with tier: stamps `fjall:disasterRecovery:tier` on the resource
|
|
58
|
+
* types AWS Backup can protect — DynamoDB tables, RDS clusters and
|
|
59
|
+
* instances, the EBS volumes Fjall keeps state on, and versioned S3
|
|
60
|
+
* buckets — and sets the app backup tier that resource defaults derive
|
|
61
|
+
* from. The backup selection built from that tag carries no resource-type
|
|
62
|
+
* filter, so the tag IS the selection: it is applied by the constructs
|
|
63
|
+
* that own those resources, not across the app. S3 buckets version by
|
|
64
|
+
* default at `resilient`/`enterprise`; a per-bucket `backupVaultTier`
|
|
65
|
+
* overrides the tier for that bucket.
|
|
61
66
|
* - false: Explicitly disabled (no backup tag, no tier-derived defaults)
|
|
62
67
|
*/
|
|
63
68
|
backup?: {
|
|
@@ -148,14 +153,16 @@ export declare class App extends CdkApp {
|
|
|
148
153
|
*/
|
|
149
154
|
getStack(key: string, dependencies?: AwsStack | AwsStack[], props?: AwsStackProps): AwsStack;
|
|
150
155
|
/**
|
|
151
|
-
* Retrieve default compute stack
|
|
156
|
+
* Retrieve default compute stack, named by
|
|
157
|
+
* `getApplicationStackName(name, "Compute")`.
|
|
152
158
|
*
|
|
153
159
|
* Only depends on Network. Database dependency is added automatically
|
|
154
160
|
* by CDK when compute resources reference database resources.
|
|
155
161
|
*/
|
|
156
162
|
getDefaultComputeStack(): AwsStack;
|
|
157
163
|
/**
|
|
158
|
-
* Retrieve default network stack
|
|
164
|
+
* Retrieve default network stack, named by
|
|
165
|
+
* `getApplicationStackName(name, "Network")`.
|
|
159
166
|
*/
|
|
160
167
|
getDefaultNetworkStack(): AwsStack;
|
|
161
168
|
/**
|
|
@@ -171,15 +178,18 @@ export declare class App extends CdkApp {
|
|
|
171
178
|
*/
|
|
172
179
|
private networkDependency;
|
|
173
180
|
/**
|
|
174
|
-
* Retrieve default database stack
|
|
181
|
+
* Retrieve default database stack, named by
|
|
182
|
+
* `getApplicationStackName(name, "Database")`.
|
|
175
183
|
*/
|
|
176
184
|
getDefaultDatabaseStack(): AwsStack;
|
|
177
185
|
/**
|
|
178
|
-
* Retrieve default storage stack
|
|
186
|
+
* Retrieve default storage stack, named by
|
|
187
|
+
* `getApplicationStackName(name, "Storage")`.
|
|
179
188
|
*/
|
|
180
189
|
getDefaultStorageStack(): AwsStack;
|
|
181
190
|
/**
|
|
182
|
-
* Retrieve default CDN stack
|
|
191
|
+
* Retrieve default CDN stack, named by
|
|
192
|
+
* `getApplicationStackName(name, "Cdn")`.
|
|
183
193
|
*
|
|
184
194
|
* Depends on Network when networking is enabled. Compute/Storage dependencies
|
|
185
195
|
* are added automatically by CDK when CDN resources reference ALB or S3 bucket
|
|
@@ -189,7 +199,8 @@ export declare class App extends CdkApp {
|
|
|
189
199
|
*/
|
|
190
200
|
getDefaultCdnStack(): AwsStack;
|
|
191
201
|
/**
|
|
192
|
-
* Retrieve default messaging stack
|
|
202
|
+
* Retrieve default messaging stack, named by
|
|
203
|
+
* `getApplicationStackName(name, "Messaging")`.
|
|
193
204
|
*
|
|
194
205
|
* Used for SQS queues, SNS topics, and EventBridge event buses.
|
|
195
206
|
* Depends on Network only. These are regional services that don't
|
|
@@ -197,8 +208,9 @@ export declare class App extends CdkApp {
|
|
|
197
208
|
*/
|
|
198
209
|
getDefaultMessagingStack(): AwsStack;
|
|
199
210
|
/**
|
|
200
|
-
* Retrieve the app-owned us-east-1 certificate stack
|
|
201
|
-
*
|
|
211
|
+
* Retrieve the app-owned us-east-1 certificate stack, named by
|
|
212
|
+
* `getApplicationStackName(name, "UsEast1Certificates")` (domain
|
|
213
|
+
* gold-plating D3, BYO path).
|
|
202
214
|
*
|
|
203
215
|
* CloudFront accepts viewer certificates from us-east-1 only, so
|
|
204
216
|
* CloudFront-consuming patterns (staticsite, payload) whose app deploys to
|
|
@@ -211,6 +223,34 @@ export declare class App extends CdkApp {
|
|
|
211
223
|
* Custom::CrossRegionExportWriter/Reader pair is pinned by template test.
|
|
212
224
|
*/
|
|
213
225
|
getUsEast1CertificateStack(): AwsStack;
|
|
226
|
+
/**
|
|
227
|
+
* Retrieve the app-owned us-east-1 CloudFront logging stack — named
|
|
228
|
+
* `${this.name}UsEast1CdnLogging`.
|
|
229
|
+
*
|
|
230
|
+
* CloudWatch Logs accepts CloudFront standard-logging (v2) delivery
|
|
231
|
+
* sources, destinations and deliveries in us-east-1 only, so a logging
|
|
232
|
+
* CDN whose app deploys to another region creates them here; the
|
|
233
|
+
* distribution ARN and the log bucket ARN reach this stack through CDK
|
|
234
|
+
* cross-region references, which makes it a CONSUMER of the Cdn stack —
|
|
235
|
+
* it deploys after the Cdn stack and is destroyed before it, the mirror
|
|
236
|
+
* of the certificate stack. A CDN in a us-east-1 app never creates it.
|
|
237
|
+
*
|
|
238
|
+
* Everything crossing the boundary is a value the export writer requires
|
|
239
|
+
* stable under an unchanged logical id: the distribution ARN and the
|
|
240
|
+
* bucket ARN both are. Never route a per-synth value (a timestamp, an
|
|
241
|
+
* asset hash) through this stack's references.
|
|
242
|
+
*/
|
|
243
|
+
getUsEast1CdnLoggingStack(): AwsStack;
|
|
244
|
+
/**
|
|
245
|
+
* The two us-east-1 stacks share a shape: no stack dependencies of their
|
|
246
|
+
* own (the CDK cross-region reference adds the consumer→producer edge),
|
|
247
|
+
* env-pinned to the deploying account (cross-region references resolve
|
|
248
|
+
* only between env-pinned stacks) and `crossRegionReferences: true` — the
|
|
249
|
+
* flag must be on BOTH the producing and the consuming stack, and the
|
|
250
|
+
* emitted Custom::CrossRegionExportWriter/Reader pair is pinned by
|
|
251
|
+
* template test.
|
|
252
|
+
*/
|
|
253
|
+
private getUsEast1Stack;
|
|
214
254
|
/**
|
|
215
255
|
* Get a VPC by name. If no name is provided, returns the default VPC.
|
|
216
256
|
*
|
package/dist/lib/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { App as CdkApp, Aspects, Tags } from "aws-cdk-lib";
|
|
2
|
-
import { maskSensitiveOutput } from "@fjall/util";
|
|
2
|
+
import { APPLICATION_STACKS, defaultVpcConstructId, getApplicationStackName, maskSensitiveOutput } from "@fjall/util";
|
|
3
3
|
import { RDS_PREVENT_RENDERING_DEPRECATED_CREDENTIALS } from "aws-cdk-lib/cx-api";
|
|
4
4
|
import { Vpc } from "./resources/aws/networking/vpc.js";
|
|
5
5
|
import { createBastion } from "./patterns/aws/bastionFactory.js";
|
|
@@ -16,7 +16,6 @@ import { DeployTimeWriterMarkerAspect } from "./utils/deployTimeWriterMarkerAspe
|
|
|
16
16
|
import { emitAppLevelAlbAliasExports } from "./utils/albAliasTargetRegistry.js";
|
|
17
17
|
import { emitAppLevelCdnAliasExports } from "./utils/cdnAliasTargetRegistry.js";
|
|
18
18
|
import { ensureFunctionLogGroups } from "./resources/aws/logging/logGroupHygiene.js";
|
|
19
|
-
import { BACKUP_TIER_TAG_KEY, BACKUP_TIER_TAG_MAP } from "./utils/backupTierMapping.js";
|
|
20
19
|
import { randomBytes } from "crypto";
|
|
21
20
|
import { getConfig } from "./utils/getConfig.js";
|
|
22
21
|
import AuditRoleFactory from "./resources/aws/audit/auditRole.js";
|
|
@@ -90,12 +89,12 @@ export class App extends CdkApp {
|
|
|
90
89
|
applyBackupTier(tier) {
|
|
91
90
|
if (this.backupTierObserved) {
|
|
92
91
|
throw new Error(`Backup tier "${tier}" applied after a resource already resolved the app's tier: ` +
|
|
93
|
-
`that resource
|
|
92
|
+
`that resource is already built at the tier it read — its versioning default, ` +
|
|
93
|
+
`its backup enrolment and every other tier-owned default are synthesised. ` +
|
|
94
94
|
`Pass \`backup\` on the first App.getApp("${this.name}") call, before addStorage(), ` +
|
|
95
95
|
`addBuildkite() or a Payload pattern.`);
|
|
96
96
|
}
|
|
97
97
|
this.backupTier = tier;
|
|
98
|
-
this.globalTags[BACKUP_TIER_TAG_KEY] = BACKUP_TIER_TAG_MAP[tier];
|
|
99
98
|
}
|
|
100
99
|
initialiseTunnel(config) {
|
|
101
100
|
if (this.networkDisabled || !this.vpc) {
|
|
@@ -113,7 +112,7 @@ export class App extends CdkApp {
|
|
|
113
112
|
}
|
|
114
113
|
else {
|
|
115
114
|
const networkConfig = { ...config, vpcName: config.vpcName ?? this.name };
|
|
116
|
-
const network = NetworkFactory.build(
|
|
115
|
+
const network = NetworkFactory.build(defaultVpcConstructId(this.name), networkConfig)(this, networkStack.getStack());
|
|
117
116
|
this.vpc = network.getVpc();
|
|
118
117
|
}
|
|
119
118
|
}
|
|
@@ -146,7 +145,7 @@ export class App extends CdkApp {
|
|
|
146
145
|
// Warn if network already initialised - changing name after network exists creates a mismatch
|
|
147
146
|
if (App.instance.vpc) {
|
|
148
147
|
FjallLogger.warn(`App name changed from "${App.instance.name}" to "${name}" after network was initialised. ` +
|
|
149
|
-
`VPC will
|
|
148
|
+
`VPC construct id will be "${defaultVpcConstructId(App.instance.name)}" but app name is "${name}". ` +
|
|
150
149
|
`Ensure App.getApp() is called with the final name before initialising network.`);
|
|
151
150
|
}
|
|
152
151
|
App.instance.name = name;
|
|
@@ -230,19 +229,21 @@ export class App extends CdkApp {
|
|
|
230
229
|
return this.stacks[key];
|
|
231
230
|
}
|
|
232
231
|
/**
|
|
233
|
-
* Retrieve default compute stack
|
|
232
|
+
* Retrieve default compute stack, named by
|
|
233
|
+
* `getApplicationStackName(name, "Compute")`.
|
|
234
234
|
*
|
|
235
235
|
* Only depends on Network. Database dependency is added automatically
|
|
236
236
|
* by CDK when compute resources reference database resources.
|
|
237
237
|
*/
|
|
238
238
|
getDefaultComputeStack() {
|
|
239
|
-
return this.getStack(
|
|
239
|
+
return this.getStack(getApplicationStackName(this.name, APPLICATION_STACKS.COMPUTE), this.networkDependency());
|
|
240
240
|
}
|
|
241
241
|
/**
|
|
242
|
-
* Retrieve default network stack
|
|
242
|
+
* Retrieve default network stack, named by
|
|
243
|
+
* `getApplicationStackName(name, "Network")`.
|
|
243
244
|
*/
|
|
244
245
|
getDefaultNetworkStack() {
|
|
245
|
-
return this.getStack(
|
|
246
|
+
return this.getStack(getApplicationStackName(this.name, APPLICATION_STACKS.NETWORK));
|
|
246
247
|
}
|
|
247
248
|
/**
|
|
248
249
|
* The Network stack as a dependency, or `undefined` when the app is
|
|
@@ -259,19 +260,22 @@ export class App extends CdkApp {
|
|
|
259
260
|
return this.networkDisabled ? undefined : this.getDefaultNetworkStack();
|
|
260
261
|
}
|
|
261
262
|
/**
|
|
262
|
-
* Retrieve default database stack
|
|
263
|
+
* Retrieve default database stack, named by
|
|
264
|
+
* `getApplicationStackName(name, "Database")`.
|
|
263
265
|
*/
|
|
264
266
|
getDefaultDatabaseStack() {
|
|
265
|
-
return this.getStack(
|
|
267
|
+
return this.getStack(getApplicationStackName(this.name, APPLICATION_STACKS.DATABASE), this.networkDependency());
|
|
266
268
|
}
|
|
267
269
|
/**
|
|
268
|
-
* Retrieve default storage stack
|
|
270
|
+
* Retrieve default storage stack, named by
|
|
271
|
+
* `getApplicationStackName(name, "Storage")`.
|
|
269
272
|
*/
|
|
270
273
|
getDefaultStorageStack() {
|
|
271
|
-
return this.getStack(
|
|
274
|
+
return this.getStack(getApplicationStackName(this.name, APPLICATION_STACKS.STORAGE), this.networkDependency());
|
|
272
275
|
}
|
|
273
276
|
/**
|
|
274
|
-
* Retrieve default CDN stack
|
|
277
|
+
* Retrieve default CDN stack, named by
|
|
278
|
+
* `getApplicationStackName(name, "Cdn")`.
|
|
275
279
|
*
|
|
276
280
|
* Depends on Network when networking is enabled. Compute/Storage dependencies
|
|
277
281
|
* are added automatically by CDK when CDN resources reference ALB or S3 bucket
|
|
@@ -283,23 +287,23 @@ export class App extends CdkApp {
|
|
|
283
287
|
// crossRegionReferences is enabled at creation so the Cdn stack can
|
|
284
288
|
// consume a us-east-1 certificate (domain gold-plating D3, BYO path).
|
|
285
289
|
// The flag is inert unless a cross-region reference is synthesised.
|
|
286
|
-
return this.getStack(
|
|
287
|
-
crossRegionReferences: true
|
|
288
|
-
});
|
|
290
|
+
return this.getStack(getApplicationStackName(this.name, APPLICATION_STACKS.CDN), this.networkDependency(), { crossRegionReferences: true });
|
|
289
291
|
}
|
|
290
292
|
/**
|
|
291
|
-
* Retrieve default messaging stack
|
|
293
|
+
* Retrieve default messaging stack, named by
|
|
294
|
+
* `getApplicationStackName(name, "Messaging")`.
|
|
292
295
|
*
|
|
293
296
|
* Used for SQS queues, SNS topics, and EventBridge event buses.
|
|
294
297
|
* Depends on Network only. These are regional services that don't
|
|
295
298
|
* require VPC, but we maintain consistent stack dependency patterns.
|
|
296
299
|
*/
|
|
297
300
|
getDefaultMessagingStack() {
|
|
298
|
-
return this.getStack(
|
|
301
|
+
return this.getStack(getApplicationStackName(this.name, APPLICATION_STACKS.MESSAGING), this.networkDependency());
|
|
299
302
|
}
|
|
300
303
|
/**
|
|
301
|
-
* Retrieve the app-owned us-east-1 certificate stack
|
|
302
|
-
*
|
|
304
|
+
* Retrieve the app-owned us-east-1 certificate stack, named by
|
|
305
|
+
* `getApplicationStackName(name, "UsEast1Certificates")` (domain
|
|
306
|
+
* gold-plating D3, BYO path).
|
|
303
307
|
*
|
|
304
308
|
* CloudFront accepts viewer certificates from us-east-1 only, so
|
|
305
309
|
* CloudFront-consuming patterns (staticsite, payload) whose app deploys to
|
|
@@ -312,11 +316,43 @@ export class App extends CdkApp {
|
|
|
312
316
|
* Custom::CrossRegionExportWriter/Reader pair is pinned by template test.
|
|
313
317
|
*/
|
|
314
318
|
getUsEast1CertificateStack() {
|
|
315
|
-
|
|
319
|
+
return this.getUsEast1Stack(APPLICATION_STACKS.US_EAST_1_CERTIFICATES, "certificate");
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Retrieve the app-owned us-east-1 CloudFront logging stack — named
|
|
323
|
+
* `${this.name}UsEast1CdnLogging`.
|
|
324
|
+
*
|
|
325
|
+
* CloudWatch Logs accepts CloudFront standard-logging (v2) delivery
|
|
326
|
+
* sources, destinations and deliveries in us-east-1 only, so a logging
|
|
327
|
+
* CDN whose app deploys to another region creates them here; the
|
|
328
|
+
* distribution ARN and the log bucket ARN reach this stack through CDK
|
|
329
|
+
* cross-region references, which makes it a CONSUMER of the Cdn stack —
|
|
330
|
+
* it deploys after the Cdn stack and is destroyed before it, the mirror
|
|
331
|
+
* of the certificate stack. A CDN in a us-east-1 app never creates it.
|
|
332
|
+
*
|
|
333
|
+
* Everything crossing the boundary is a value the export writer requires
|
|
334
|
+
* stable under an unchanged logical id: the distribution ARN and the
|
|
335
|
+
* bucket ARN both are. Never route a per-synth value (a timestamp, an
|
|
336
|
+
* asset hash) through this stack's references.
|
|
337
|
+
*/
|
|
338
|
+
getUsEast1CdnLoggingStack() {
|
|
339
|
+
return this.getUsEast1Stack(APPLICATION_STACKS.US_EAST_1_CDN_LOGGING, "CloudFront logging");
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The two us-east-1 stacks share a shape: no stack dependencies of their
|
|
343
|
+
* own (the CDK cross-region reference adds the consumer→producer edge),
|
|
344
|
+
* env-pinned to the deploying account (cross-region references resolve
|
|
345
|
+
* only between env-pinned stacks) and `crossRegionReferences: true` — the
|
|
346
|
+
* flag must be on BOTH the producing and the consuming stack, and the
|
|
347
|
+
* emitted Custom::CrossRegionExportWriter/Reader pair is pinned by
|
|
348
|
+
* template test.
|
|
349
|
+
*/
|
|
350
|
+
getUsEast1Stack(stack, purpose) {
|
|
351
|
+
const key = getApplicationStackName(this.name, stack);
|
|
316
352
|
if (!this.stacks[key]) {
|
|
317
353
|
const account = process.env.CDK_DEFAULT_ACCOUNT;
|
|
318
354
|
if (!account) {
|
|
319
|
-
throw new Error(`App '${this.name}': the us-east-1
|
|
355
|
+
throw new Error(`App '${this.name}': the us-east-1 ${purpose} stack needs a ` +
|
|
320
356
|
"concrete account (got CDK_DEFAULT_ACCOUNT undefined). " +
|
|
321
357
|
"Cross-region references resolve only between env-pinned stacks " +
|
|
322
358
|
"— deploy through the Fjall CLI, or export CDK_DEFAULT_ACCOUNT " +
|
|
@@ -25,6 +25,9 @@ export class ConfigRecorder extends Construct {
|
|
|
25
25
|
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
26
26
|
versioned: true,
|
|
27
27
|
removalPolicy: RemovalPolicy.RETAIN,
|
|
28
|
+
// AWS Config delivers configuration snapshots here on its own schedule
|
|
29
|
+
// and the 90-day rule below is the retention; nothing restores from it.
|
|
30
|
+
backupExemption: "log-delivery",
|
|
28
31
|
lifecycleRules: [
|
|
29
32
|
{
|
|
30
33
|
expiration: Duration.days(90),
|
|
@@ -12,8 +12,7 @@ export interface DisasterRecoveryProps {
|
|
|
12
12
|
customBackupPlans?: CustomBackupPlanConfig[];
|
|
13
13
|
}
|
|
14
14
|
export declare class DisasterRecovery extends Construct {
|
|
15
|
-
readonly backupVault
|
|
16
|
-
private readonly vaultRef;
|
|
15
|
+
readonly backupVault: BackupVault;
|
|
17
16
|
readonly backupPlans: {
|
|
18
17
|
standard: BackupPlan;
|
|
19
18
|
resilient: BackupPlan;
|
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import { CfnOutput, Duration, RemovalPolicy } from "aws-cdk-lib";
|
|
1
|
+
import { Annotations, CfnOutput, Duration, RemovalPolicy } from "aws-cdk-lib";
|
|
2
2
|
import { Construct } from "constructs";
|
|
3
3
|
import { BackupVault, BackupPlan } from "../../resources/aws/backup/index.js";
|
|
4
4
|
import { BackupPlanRule, BackupVault as Vault } from "aws-cdk-lib/aws-backup";
|
|
5
|
-
import { cronSchedule } from "../../resources/aws/messaging/index.js";
|
|
6
5
|
import { AccountPrincipal, Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
6
|
+
import { Key } from "aws-cdk-lib/aws-kms";
|
|
7
|
+
import { cronSchedule } from "../../resources/aws/messaging/index.js";
|
|
7
8
|
import { getConfig } from "../../utils/getConfig.js";
|
|
8
9
|
import { toPascalCase } from "../../utils/capitaliseString.js";
|
|
9
|
-
import {
|
|
10
|
+
import { readBackupVaultVerdict } from "../../utils/cdkContext.js";
|
|
11
|
+
import { BACKUP_VAULT_NAME, isReownedBackupVault } from "@fjall/util";
|
|
10
12
|
// Backup retention constants (in days)
|
|
11
13
|
const RETENTION_PERIODS = {
|
|
12
14
|
STANDARD: 90,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
// AWS caps a continuous-backup window at 35 days, so the resilient and the
|
|
16
|
+
// enterprise continuous rules both sit at the cap: it is AWS's limit, not a
|
|
17
|
+
// tier's choice, which is why one constant serves both.
|
|
18
|
+
CONTINUOUS_MAX: 35,
|
|
15
19
|
ENTERPRISE_COMPLIANCE: 2555, // 7 years
|
|
16
|
-
COLD_STORAGE_MINIMUM: 90
|
|
17
|
-
COLD_STORAGE_ONE_YEAR: 365
|
|
20
|
+
COLD_STORAGE_MINIMUM: 90 // AWS's minimum before a cold point may be deleted
|
|
18
21
|
};
|
|
19
22
|
// Backup window constants
|
|
20
23
|
const BACKUP_WINDOWS = {
|
|
@@ -28,11 +31,13 @@ const VAULT_LOCK = {
|
|
|
28
31
|
MAX_RETENTION: Duration.days(36500),
|
|
29
32
|
GRACE_PERIOD: Duration.days(3)
|
|
30
33
|
};
|
|
34
|
+
function liveLockMode(lock) {
|
|
35
|
+
if (lock === null)
|
|
36
|
+
return "none";
|
|
37
|
+
return lock.lockDate !== undefined ? "compliance" : "governance";
|
|
38
|
+
}
|
|
31
39
|
export class DisasterRecovery extends Construct {
|
|
32
|
-
// Set in create-mode only; in adopt-mode the existing vault is referenced, not created.
|
|
33
40
|
backupVault;
|
|
34
|
-
// Vault the plans write to: created vault (create-mode) or imported handle (adopt-mode).
|
|
35
|
-
vaultRef;
|
|
36
41
|
backupPlans;
|
|
37
42
|
customBackupPlans;
|
|
38
43
|
constructor(scope, id, props) {
|
|
@@ -41,9 +46,10 @@ export class DisasterRecovery extends Construct {
|
|
|
41
46
|
const providerAccounts = config.providerAccounts;
|
|
42
47
|
const account = providerAccounts.find((pa) => pa.id === props.accountId);
|
|
43
48
|
const isComplianceAccount = account?.environment === "compliance";
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
const
|
|
49
|
+
// Injected by the deploy engine after probing the live vault and the account
|
|
50
|
+
// stack; unset (a bare synth) is the create template.
|
|
51
|
+
const verdict = readBackupVaultVerdict(this.node);
|
|
52
|
+
const reOwned = isReownedBackupVault(verdict);
|
|
47
53
|
const disasterRecoveryRegion = config.disasterRecoveryRegion;
|
|
48
54
|
// Look up compliance account for cross-account replication
|
|
49
55
|
// Skip if the compliance account (prevent self-replication)
|
|
@@ -58,9 +64,11 @@ export class DisasterRecovery extends Construct {
|
|
|
58
64
|
// permanently-immutable compliance lock. See
|
|
59
65
|
// decisions/2026-06-03-backup-vault-lock-mode-by-intent.md.
|
|
60
66
|
const vaultLock = account?.vaultLock ?? (isComplianceAccount ? "governance" : "none");
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
67
|
+
// Fjall never issues a lock call against a vault it did not create: a
|
|
68
|
+
// re-owned vault declares no lock and the intent only warns when it
|
|
69
|
+
// differs, so the acknowledgement gate guards only a lock this template
|
|
70
|
+
// creates.
|
|
71
|
+
if (!reOwned &&
|
|
64
72
|
vaultLock === "compliance" &&
|
|
65
73
|
account?.acknowledgeImmutableVaultLock !== true) {
|
|
66
74
|
throw new Error(`Account "${account?.name ?? props.accountId}" requests vaultLock: ` +
|
|
@@ -69,7 +77,7 @@ export class DisasterRecovery extends Construct {
|
|
|
69
77
|
`compliance lock cannot be removed by anyone, including AWS or the ` +
|
|
70
78
|
`root user. Set the acknowledgement explicitly to proceed.`);
|
|
71
79
|
}
|
|
72
|
-
const
|
|
80
|
+
const intendedLockConfiguration = vaultLock === "compliance"
|
|
73
81
|
? {
|
|
74
82
|
minRetention: VAULT_LOCK.MIN_RETENTION,
|
|
75
83
|
maxRetention: VAULT_LOCK.MAX_RETENTION,
|
|
@@ -81,20 +89,33 @@ export class DisasterRecovery extends Construct {
|
|
|
81
89
|
maxRetention: VAULT_LOCK.MAX_RETENTION
|
|
82
90
|
}
|
|
83
91
|
: undefined;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
const lockConfiguration = reOwned ? undefined : intendedLockConfiguration;
|
|
93
|
+
const liveLocked = verdict.state !== "absent" && verdict.lock !== null;
|
|
94
|
+
if (verdict.state !== "absent" && reOwned) {
|
|
95
|
+
const liveMode = liveLockMode(verdict.lock);
|
|
96
|
+
if (liveMode !== vaultLock) {
|
|
97
|
+
Annotations.of(this).addWarningV2("fjall:backupVault:lockIntentNotApplied", `Backup vault "${BACKUP_VAULT_NAME}" is re-owned: Fjall does not ` +
|
|
98
|
+
`manage the lock of a vault it did not create. Its live lock ` +
|
|
99
|
+
`(${liveMode}) stays as it is and the configured vaultLock ` +
|
|
100
|
+
`(${vaultLock}) is not applied — align the configured vaultLock ` +
|
|
101
|
+
`with the live lock, or change the live lock with the AWS Backup ` +
|
|
102
|
+
`API where it is still changeable.`);
|
|
103
|
+
}
|
|
94
104
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
this.backupVault = new BackupVault(this, "BackupVault", {
|
|
106
|
+
vaultName: BACKUP_VAULT_NAME,
|
|
107
|
+
removalPolicy: RemovalPolicy.RETAIN,
|
|
108
|
+
lockConfiguration,
|
|
109
|
+
...(verdict.state !== "absent" &&
|
|
110
|
+
reOwned && {
|
|
111
|
+
encryptionKey: Key.fromKeyArn(this, "BackupVaultLiveKey", verdict.keyArn)
|
|
112
|
+
}),
|
|
113
|
+
importIdentityOnly: verdict.state === "unowned"
|
|
114
|
+
});
|
|
115
|
+
const vault = this.backupVault.vault;
|
|
116
|
+
// Cross-account copy grant. Declared in every mode; the wrapper strips it
|
|
117
|
+
// from the import pass, and the converge deploy applies it.
|
|
118
|
+
if (isComplianceAccount) {
|
|
98
119
|
const allAccounts = providerAccounts;
|
|
99
120
|
const productionAccounts = allAccounts.filter((acc) => acc.environment === "production");
|
|
100
121
|
if (productionAccounts.length > 0) {
|
|
@@ -104,7 +125,52 @@ export class DisasterRecovery extends Construct {
|
|
|
104
125
|
actions: ["backup:CopyIntoBackupVault"],
|
|
105
126
|
resources: ["*"]
|
|
106
127
|
});
|
|
107
|
-
|
|
128
|
+
vault.addToAccessPolicy(copyStatement);
|
|
129
|
+
// The access policy admits the copy; the KEY has to admit it too. A
|
|
130
|
+
// copy job into an encrypted vault in another account is executed by
|
|
131
|
+
// the SOURCE account, so without these the job fails at the
|
|
132
|
+
// destination with an access-denied nothing in the source account
|
|
133
|
+
// surfaces. The enterprise tier's seven-year record is this copy, so
|
|
134
|
+
// the grant is load-bearing, not hardening.
|
|
135
|
+
const sourcePrincipals = productionAccounts.map((acc) => new AccountPrincipal(acc.id));
|
|
136
|
+
if (this.backupVault.ownsEncryptionKey) {
|
|
137
|
+
this.backupVault.encryptionKey.addToResourcePolicy(new PolicyStatement({
|
|
138
|
+
sid: "FjallAllowCrossAccountBackupCopyKeyUse",
|
|
139
|
+
effect: Effect.ALLOW,
|
|
140
|
+
principals: sourcePrincipals,
|
|
141
|
+
actions: [
|
|
142
|
+
"kms:Decrypt",
|
|
143
|
+
"kms:DescribeKey",
|
|
144
|
+
"kms:Encrypt",
|
|
145
|
+
"kms:GenerateDataKey",
|
|
146
|
+
"kms:GenerateDataKeyWithoutPlaintext",
|
|
147
|
+
"kms:ReEncryptFrom",
|
|
148
|
+
"kms:ReEncryptTo"
|
|
149
|
+
],
|
|
150
|
+
resources: ["*"]
|
|
151
|
+
}));
|
|
152
|
+
this.backupVault.encryptionKey.addToResourcePolicy(new PolicyStatement({
|
|
153
|
+
sid: "FjallAllowCrossAccountBackupCopyGrant",
|
|
154
|
+
effect: Effect.ALLOW,
|
|
155
|
+
principals: sourcePrincipals,
|
|
156
|
+
actions: ["kms:CreateGrant"],
|
|
157
|
+
resources: ["*"],
|
|
158
|
+
conditions: {
|
|
159
|
+
Bool: { "kms:GrantIsForAWSResource": "true" }
|
|
160
|
+
}
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
Annotations.of(this).addWarningV2("fjall:backupVault:crossAccountKeyGrantNotApplied", `Fjall does not manage the encryption key of backup vault ` +
|
|
165
|
+
`"${BACKUP_VAULT_NAME}" — the vault was re-owned, so the key is ` +
|
|
166
|
+
`yours — and cannot grant the source accounts use of it. ` +
|
|
167
|
+
`Cross-account copies into this vault fail at the copy job ` +
|
|
168
|
+
`until the key policy admits them: add ` +
|
|
169
|
+
`kms:Decrypt, kms:Encrypt, kms:GenerateDataKey*, kms:ReEncrypt* ` +
|
|
170
|
+
`and a kms:CreateGrant statement conditioned on ` +
|
|
171
|
+
`kms:GrantIsForAWSResource for the production accounts, from a ` +
|
|
172
|
+
`principal in the compliance account.`);
|
|
173
|
+
}
|
|
108
174
|
}
|
|
109
175
|
}
|
|
110
176
|
// Construct cross-region replication vault ARN (same account, DR region)
|
|
@@ -118,19 +184,19 @@ export class DisasterRecovery extends Construct {
|
|
|
118
184
|
planName: "standard",
|
|
119
185
|
rules: this.createStandardBackupRules("standard"),
|
|
120
186
|
tagValue: "default",
|
|
121
|
-
backupVault:
|
|
187
|
+
backupVault: vault
|
|
122
188
|
}),
|
|
123
189
|
resilient: new BackupPlan(this, "ResilientBackupPlan", {
|
|
124
190
|
planName: "resilient",
|
|
125
191
|
rules: this.createResilientBackupRules("resilient", replicationVaultArn),
|
|
126
192
|
tagValue: "resilient",
|
|
127
|
-
backupVault:
|
|
193
|
+
backupVault: vault
|
|
128
194
|
}),
|
|
129
195
|
enterprise: new BackupPlan(this, "EnterpriseBackupPlan", {
|
|
130
196
|
planName: "enterprise",
|
|
131
197
|
rules: this.createEnterpriseBackupRules("enterprise", replicationVaultArn, disasterRecoveryVaultArn),
|
|
132
198
|
tagValue: "enterprise",
|
|
133
|
-
backupVault:
|
|
199
|
+
backupVault: vault
|
|
134
200
|
})
|
|
135
201
|
};
|
|
136
202
|
// Create custom backup plans if provided
|
|
@@ -140,7 +206,7 @@ export class DisasterRecovery extends Construct {
|
|
|
140
206
|
planName: config.planName,
|
|
141
207
|
rules: config.rules,
|
|
142
208
|
tagValue: config.tagValue,
|
|
143
|
-
backupVault:
|
|
209
|
+
backupVault: vault
|
|
144
210
|
});
|
|
145
211
|
});
|
|
146
212
|
}
|
|
@@ -157,7 +223,7 @@ export class DisasterRecovery extends Construct {
|
|
|
157
223
|
exportName: `${id}ReplicationRegion`
|
|
158
224
|
});
|
|
159
225
|
}
|
|
160
|
-
if (
|
|
226
|
+
if (reOwned ? liveLocked : lockConfiguration !== undefined) {
|
|
161
227
|
new CfnOutput(this, "VaultLockEnabled", {
|
|
162
228
|
key: "VaultLockEnabled",
|
|
163
229
|
value: "true",
|
|
@@ -177,12 +243,12 @@ export class DisasterRecovery extends Construct {
|
|
|
177
243
|
];
|
|
178
244
|
}
|
|
179
245
|
createResilientBackupRules(planName, replicationVaultArn) {
|
|
180
|
-
const copyActions = this.createCopyActions(planName, replicationVaultArn, undefined, Duration.days(RETENTION_PERIODS.
|
|
246
|
+
const copyActions = this.createCopyActions(planName, replicationVaultArn, undefined, Duration.days(RETENTION_PERIODS.CONTINUOUS_MAX), undefined);
|
|
181
247
|
return [
|
|
182
248
|
new BackupPlanRule({
|
|
183
249
|
ruleName: `${planName}Daily`,
|
|
184
250
|
scheduleExpression: cronSchedule({ hour: "2", minute: "0" }), // 2 AM daily
|
|
185
|
-
deleteAfter: Duration.days(RETENTION_PERIODS.
|
|
251
|
+
deleteAfter: Duration.days(RETENTION_PERIODS.CONTINUOUS_MAX),
|
|
186
252
|
enableContinuousBackup: true,
|
|
187
253
|
startWindow: BACKUP_WINDOWS.START,
|
|
188
254
|
completionWindow: BACKUP_WINDOWS.COMPLETION,
|
|
@@ -191,25 +257,81 @@ export class DisasterRecovery extends Construct {
|
|
|
191
257
|
];
|
|
192
258
|
}
|
|
193
259
|
createEnterpriseBackupRules(planName, replicationVaultArn, disasterRecoveryVaultArn) {
|
|
194
|
-
|
|
195
|
-
|
|
260
|
+
// One destination per rule, by role. The continuous window is the
|
|
261
|
+
// regional-outage answer and goes to the DR region in this account; the
|
|
262
|
+
// seven-year record goes to the compliance account and nowhere else.
|
|
263
|
+
// Sending both rules to both vaults doubled the copy volume to buy a
|
|
264
|
+
// second copy of the same thing.
|
|
265
|
+
//
|
|
266
|
+
// Each rule falls back to the other's destination when its own role has
|
|
267
|
+
// none, because the two ARNs go absent for independent reasons: the
|
|
268
|
+
// cross-account one whenever the organisation has no compliance account,
|
|
269
|
+
// and inside the compliance account itself where the self-replication
|
|
270
|
+
// guard clears it; the DR-region one whenever the configured
|
|
271
|
+
// disasterRecoveryRegion IS this account's region. A rule with no
|
|
272
|
+
// destination keeps every recovery point it takes in one vault in one
|
|
273
|
+
// region, which is the failure the tier exists to answer, so one copy in
|
|
274
|
+
// the less appropriate place beats none. Still one destination per rule:
|
|
275
|
+
// each fallback is passed only when that rule's own destination is absent.
|
|
276
|
+
const continuousCopyActions = this.createCopyActions(`${planName}Continuous`, replicationVaultArn, replicationVaultArn === undefined ? disasterRecoveryVaultArn : undefined, Duration.days(RETENTION_PERIODS.CONTINUOUS_MAX), undefined);
|
|
277
|
+
const complianceCopyActions = this.createCopyActions(`${planName}Compliance`, disasterRecoveryVaultArn === undefined ? replicationVaultArn : undefined, disasterRecoveryVaultArn, Duration.days(RETENTION_PERIODS.ENTERPRISE_COMPLIANCE), Duration.days(RETENTION_PERIODS.COLD_STORAGE_MINIMUM));
|
|
278
|
+
// Both candidate ARNs name `disasterRecoveryRegion`, so an absent
|
|
279
|
+
// `replicationVaultArn` means that region IS this account's own and
|
|
280
|
+
// nothing either rule can reach crosses a region — whatever destination
|
|
281
|
+
// the fallbacks above found. The account stack builds this construct only
|
|
282
|
+
// when a DR region is configured, so this is a misconfiguration rather
|
|
283
|
+
// than an opt-out, and the tier's whole promise rests on the copy.
|
|
284
|
+
//
|
|
285
|
+
// Warn on the plan, not on one rule. Asking only whether the compliance
|
|
286
|
+
// record had a destination was silent for the configuration where a
|
|
287
|
+
// compliance account exists and the DR region is the account's own: the
|
|
288
|
+
// record copies cross-account, the continuous window copies nowhere, and
|
|
289
|
+
// the condition that fires the warning never holds.
|
|
290
|
+
if (replicationVaultArn === undefined) {
|
|
291
|
+
if (continuousCopyActions === undefined) {
|
|
292
|
+
Annotations.of(this).addWarningV2("fjall:backupPlan:recoveryPointsNotCopied", `Neither enterprise rule has a copy destination, so every recovery ` +
|
|
293
|
+
`point the plan takes — the ` +
|
|
294
|
+
`${RETENTION_PERIODS.CONTINUOUS_MAX}-day continuous window and ` +
|
|
295
|
+
`the ${RETENTION_PERIODS.ENTERPRISE_COMPLIANCE}-day compliance ` +
|
|
296
|
+
`record alike — lives in one vault in one region. The configured ` +
|
|
297
|
+
`disasterRecoveryRegion is this account's own region and the ` +
|
|
298
|
+
`organisation has no compliance account — set a ` +
|
|
299
|
+
`disasterRecoveryRegion that differs from the account's region, ` +
|
|
300
|
+
`or add a compliance account for the plan to copy to.`);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
Annotations.of(this).addWarningV2("fjall:backupPlan:noCrossRegionCopy", `The enterprise plan copies to the compliance account, but nothing ` +
|
|
304
|
+
`it copies crosses a region: the configured ` +
|
|
305
|
+
`disasterRecoveryRegion is this account's own region, so both ` +
|
|
306
|
+
`rules' recovery points stay where the originals are. Losing the ` +
|
|
307
|
+
`region is the failure this tier exists to answer — set a ` +
|
|
308
|
+
`disasterRecoveryRegion that differs from the account's region.`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
196
311
|
return [
|
|
197
312
|
// Continuous backup (35-day max PITR window)
|
|
198
313
|
new BackupPlanRule({
|
|
199
314
|
ruleName: `${planName}Continuous`,
|
|
200
315
|
scheduleExpression: cronSchedule({ hour: "2", minute: "0" }), // Daily for continuous backup
|
|
201
|
-
deleteAfter: Duration.days(RETENTION_PERIODS.
|
|
316
|
+
deleteAfter: Duration.days(RETENTION_PERIODS.CONTINUOUS_MAX),
|
|
202
317
|
enableContinuousBackup: true,
|
|
203
318
|
startWindow: BACKUP_WINDOWS.START,
|
|
204
319
|
completionWindow: BACKUP_WINDOWS.COMPLETION,
|
|
205
320
|
copyActions: continuousCopyActions
|
|
206
321
|
}),
|
|
207
|
-
//
|
|
322
|
+
// Monthly snapshot for the long-term compliance record (7 years),
|
|
323
|
+
// cold from day 90. Monthly, not hourly: inside the 35-day window the
|
|
324
|
+
// continuous rule above already gives second-level recovery, so an
|
|
325
|
+
// hourly snapshot bought granularity nobody asks for beyond 35 days at
|
|
326
|
+
// 720× the job count — and 24 RDS recovery points a day would exhaust
|
|
327
|
+
// the account's manual-snapshot headroom in under four days. Cold
|
|
328
|
+
// storage cannot ride the continuous rule: aws-cdk-lib refuses
|
|
329
|
+
// `moveToColdStorageAfter` with `enableContinuousBackup`.
|
|
208
330
|
new BackupPlanRule({
|
|
209
|
-
ruleName: `${planName}
|
|
210
|
-
scheduleExpression: cronSchedule({ minute: "0" }),
|
|
331
|
+
ruleName: `${planName}MonthlyCompliance`,
|
|
332
|
+
scheduleExpression: cronSchedule({ day: "1", hour: "2", minute: "0" }),
|
|
211
333
|
deleteAfter: Duration.days(RETENTION_PERIODS.ENTERPRISE_COMPLIANCE),
|
|
212
|
-
moveToColdStorageAfter: Duration.days(RETENTION_PERIODS.
|
|
334
|
+
moveToColdStorageAfter: Duration.days(RETENTION_PERIODS.COLD_STORAGE_MINIMUM),
|
|
213
335
|
startWindow: BACKUP_WINDOWS.START,
|
|
214
336
|
completionWindow: BACKUP_WINDOWS.COMPLETION_SHORT,
|
|
215
337
|
copyActions: complianceCopyActions
|