@fjall/components-infrastructure 38.0.0 → 39.0.1
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 +24 -12
- package/dist/lib/app.js +30 -26
- package/dist/lib/config/aws/configRecorder.js +3 -0
- package/dist/lib/config/aws/disasterRecovery.js +115 -13
- package/dist/lib/patterns/aws/buildkite/buildkite.js +7 -2
- 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/messaging.d.ts +10 -0
- package/dist/lib/patterns/aws/messaging.js +10 -6
- package/dist/lib/patterns/aws/storage.d.ts +41 -0
- package/dist/lib/patterns/aws/storage.js +23 -8
- package/dist/lib/resources/aws/backup/backupVault.d.ts +10 -0
- package/dist/lib/resources/aws/backup/backupVault.js +12 -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 +1 -1
- package/dist/lib/resources/aws/logging/accessLogBucket.js +1 -1
- package/dist/lib/resources/aws/logging/cloudTrail.js +3 -0
- package/dist/lib/resources/aws/messaging/eventBridgeQueueGrants.d.ts +2 -0
- package/dist/lib/resources/aws/messaging/eventBridgeQueueGrants.js +2 -1
- package/dist/lib/resources/aws/messaging/eventBridgeRule.js +36 -5
- package/dist/lib/resources/aws/messaging/sqs.d.ts +36 -1
- package/dist/lib/resources/aws/messaging/sqs.js +36 -2
- package/dist/lib/resources/aws/networking/vpc.js +12 -6
- package/dist/lib/resources/aws/secrets/stackEncryptionKey.d.ts +42 -9
- package/dist/lib/resources/aws/secrets/stackEncryptionKey.js +91 -32
- 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/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
|
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,7 +316,7 @@ export class App extends CdkApp {
|
|
|
312
316
|
* Custom::CrossRegionExportWriter/Reader pair is pinned by template test.
|
|
313
317
|
*/
|
|
314
318
|
getUsEast1CertificateStack() {
|
|
315
|
-
return this.getUsEast1Stack(
|
|
319
|
+
return this.getUsEast1Stack(APPLICATION_STACKS.US_EAST_1_CERTIFICATES, "certificate");
|
|
316
320
|
}
|
|
317
321
|
/**
|
|
318
322
|
* Retrieve the app-owned us-east-1 CloudFront logging stack — named
|
|
@@ -332,7 +336,7 @@ export class App extends CdkApp {
|
|
|
332
336
|
* asset hash) through this stack's references.
|
|
333
337
|
*/
|
|
334
338
|
getUsEast1CdnLoggingStack() {
|
|
335
|
-
return this.getUsEast1Stack(
|
|
339
|
+
return this.getUsEast1Stack(APPLICATION_STACKS.US_EAST_1_CDN_LOGGING, "CloudFront logging");
|
|
336
340
|
}
|
|
337
341
|
/**
|
|
338
342
|
* The two us-east-1 stacks share a shape: no stack dependencies of their
|
|
@@ -343,8 +347,8 @@ export class App extends CdkApp {
|
|
|
343
347
|
* emitted Custom::CrossRegionExportWriter/Reader pair is pinned by
|
|
344
348
|
* template test.
|
|
345
349
|
*/
|
|
346
|
-
getUsEast1Stack(
|
|
347
|
-
const key =
|
|
350
|
+
getUsEast1Stack(stack, purpose) {
|
|
351
|
+
const key = getApplicationStackName(this.name, stack);
|
|
348
352
|
if (!this.stacks[key]) {
|
|
349
353
|
const account = process.env.CDK_DEFAULT_ACCOUNT;
|
|
350
354
|
if (!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,11 +12,12 @@ import { BACKUP_VAULT_NAME, isReownedBackupVault } from "@fjall/util";
|
|
|
12
12
|
// Backup retention constants (in days)
|
|
13
13
|
const RETENTION_PERIODS = {
|
|
14
14
|
STANDARD: 90,
|
|
15
|
-
|
|
16
|
-
|
|
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,
|
|
17
19
|
ENTERPRISE_COMPLIANCE: 2555, // 7 years
|
|
18
|
-
COLD_STORAGE_MINIMUM: 90
|
|
19
|
-
COLD_STORAGE_ONE_YEAR: 365
|
|
20
|
+
COLD_STORAGE_MINIMUM: 90 // AWS's minimum before a cold point may be deleted
|
|
20
21
|
};
|
|
21
22
|
// Backup window constants
|
|
22
23
|
const BACKUP_WINDOWS = {
|
|
@@ -125,6 +126,51 @@ export class DisasterRecovery extends Construct {
|
|
|
125
126
|
resources: ["*"]
|
|
126
127
|
});
|
|
127
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
|
+
}
|
|
128
174
|
}
|
|
129
175
|
}
|
|
130
176
|
// Construct cross-region replication vault ARN (same account, DR region)
|
|
@@ -197,12 +243,12 @@ export class DisasterRecovery extends Construct {
|
|
|
197
243
|
];
|
|
198
244
|
}
|
|
199
245
|
createResilientBackupRules(planName, replicationVaultArn) {
|
|
200
|
-
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);
|
|
201
247
|
return [
|
|
202
248
|
new BackupPlanRule({
|
|
203
249
|
ruleName: `${planName}Daily`,
|
|
204
250
|
scheduleExpression: cronSchedule({ hour: "2", minute: "0" }), // 2 AM daily
|
|
205
|
-
deleteAfter: Duration.days(RETENTION_PERIODS.
|
|
251
|
+
deleteAfter: Duration.days(RETENTION_PERIODS.CONTINUOUS_MAX),
|
|
206
252
|
enableContinuousBackup: true,
|
|
207
253
|
startWindow: BACKUP_WINDOWS.START,
|
|
208
254
|
completionWindow: BACKUP_WINDOWS.COMPLETION,
|
|
@@ -211,25 +257,81 @@ export class DisasterRecovery extends Construct {
|
|
|
211
257
|
];
|
|
212
258
|
}
|
|
213
259
|
createEnterpriseBackupRules(planName, replicationVaultArn, disasterRecoveryVaultArn) {
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
}
|
|
216
311
|
return [
|
|
217
312
|
// Continuous backup (35-day max PITR window)
|
|
218
313
|
new BackupPlanRule({
|
|
219
314
|
ruleName: `${planName}Continuous`,
|
|
220
315
|
scheduleExpression: cronSchedule({ hour: "2", minute: "0" }), // Daily for continuous backup
|
|
221
|
-
deleteAfter: Duration.days(RETENTION_PERIODS.
|
|
316
|
+
deleteAfter: Duration.days(RETENTION_PERIODS.CONTINUOUS_MAX),
|
|
222
317
|
enableContinuousBackup: true,
|
|
223
318
|
startWindow: BACKUP_WINDOWS.START,
|
|
224
319
|
completionWindow: BACKUP_WINDOWS.COMPLETION,
|
|
225
320
|
copyActions: continuousCopyActions
|
|
226
321
|
}),
|
|
227
|
-
//
|
|
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`.
|
|
228
330
|
new BackupPlanRule({
|
|
229
|
-
ruleName: `${planName}
|
|
230
|
-
scheduleExpression: cronSchedule({ minute: "0" }),
|
|
331
|
+
ruleName: `${planName}MonthlyCompliance`,
|
|
332
|
+
scheduleExpression: cronSchedule({ day: "1", hour: "2", minute: "0" }),
|
|
231
333
|
deleteAfter: Duration.days(RETENTION_PERIODS.ENTERPRISE_COMPLIANCE),
|
|
232
|
-
moveToColdStorageAfter: Duration.days(RETENTION_PERIODS.
|
|
334
|
+
moveToColdStorageAfter: Duration.days(RETENTION_PERIODS.COLD_STORAGE_MINIMUM),
|
|
233
335
|
startWindow: BACKUP_WINDOWS.START,
|
|
234
336
|
completionWindow: BACKUP_WINDOWS.COMPLETION_SHORT,
|
|
235
337
|
copyActions: complianceCopyActions
|
|
@@ -95,8 +95,13 @@ export class Buildkite extends Construct {
|
|
|
95
95
|
appBackupTier
|
|
96
96
|
});
|
|
97
97
|
// Secrets (SSH deploy keys, git credentials) must survive stack teardown
|
|
98
|
-
// and accidental overwrite
|
|
99
|
-
|
|
98
|
+
// and accidental overwrite — which is exactly what a backup plan is for,
|
|
99
|
+
// so the bucket takes the app's tier like the artefact bucket above.
|
|
100
|
+
const managedSecretsBucket = new S3Bucket(this, `${id}ManagedSecretsBucket`, {
|
|
101
|
+
versioned: true,
|
|
102
|
+
removalPolicy: RemovalPolicy.RETAIN,
|
|
103
|
+
appBackupTier
|
|
104
|
+
});
|
|
100
105
|
if (fjallApiKeyParameterName !== undefined ||
|
|
101
106
|
pipelineSecretsPrefix !== undefined) {
|
|
102
107
|
this.addAgentSecretsEnvHook(id, managedSecretsBucket, {
|
|
@@ -367,6 +367,9 @@ export class ClickHouseDatabase extends Construct {
|
|
|
367
367
|
const backupBucket = props.backupBucket ??
|
|
368
368
|
new S3Bucket(this, "ClickHouseBackupBucket", {
|
|
369
369
|
versioned: true,
|
|
370
|
+
...(props.appBackupTier !== undefined && {
|
|
371
|
+
appBackupTier: props.appBackupTier
|
|
372
|
+
}),
|
|
370
373
|
lifecycleRules: [
|
|
371
374
|
{
|
|
372
375
|
enabled: true,
|
|
@@ -525,8 +528,7 @@ export class ClickHouseDatabase extends Construct {
|
|
|
525
528
|
`${sidecarTlsPreamble}${optimisePreamble}\n${optimiseScript}`
|
|
526
529
|
],
|
|
527
530
|
secrets: maintenanceSecrets,
|
|
528
|
-
logGroup: optimiseTaskLogGroup
|
|
529
|
-
securityGroups: [securityGroup]
|
|
531
|
+
logGroup: optimiseTaskLogGroup
|
|
530
532
|
});
|
|
531
533
|
maintenanceLogGroups.push({
|
|
532
534
|
job: "optimise",
|
|
@@ -555,8 +557,7 @@ export class ClickHouseDatabase extends Construct {
|
|
|
555
557
|
})}`
|
|
556
558
|
],
|
|
557
559
|
secrets: maintenanceSecrets,
|
|
558
|
-
logGroup: backupTaskLogGroup
|
|
559
|
-
securityGroups: [securityGroup]
|
|
560
|
+
logGroup: backupTaskLogGroup
|
|
560
561
|
});
|
|
561
562
|
maintenanceLogGroups.push({
|
|
562
563
|
job: "backup",
|
|
@@ -590,6 +591,9 @@ export class ClickHouseDatabase extends Construct {
|
|
|
590
591
|
...(props.appBackupTier !== undefined && {
|
|
591
592
|
appBackupTier: props.appBackupTier
|
|
592
593
|
}),
|
|
594
|
+
// ClickHouse is infrastructure: the deploy engine must never roll it
|
|
595
|
+
// out, restart it or rebind it as one of the app's own services.
|
|
596
|
+
deployableServices: false,
|
|
593
597
|
cluster: {
|
|
594
598
|
loadBalancer: false,
|
|
595
599
|
securityGroup,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AwsLogDriver, ContainerImage, DeploymentLifecycleStage, Secret as EcsSecret } from "aws-cdk-lib/aws-ecs";
|
|
1
|
+
import { AwsLogDriver, ContainerImage, DeploymentLifecycleStage, NetworkMode, Secret as EcsSecret } from "aws-cdk-lib/aws-ecs";
|
|
2
2
|
import { Peer, Port, SubnetType } from "aws-cdk-lib/aws-ec2";
|
|
3
3
|
import { Effect, PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import { Annotations, Aws, Stack, Token } from "aws-cdk-lib";
|
|
@@ -269,6 +269,21 @@ export function validateEcsProps(props) {
|
|
|
269
269
|
"does not own. Either omit cluster.securityGroup (a Fjall-owned SG will be created) or set directAccess: false.");
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
|
+
for (const entry of props.cluster?.scheduledTasks ?? []) {
|
|
273
|
+
if (entry.networkMode === NetworkMode.AWS_VPC) {
|
|
274
|
+
throw new Error(`Scheduled task '${entry.name}': networkMode AWS_VPC is not supported for scheduled tasks — ` +
|
|
275
|
+
"a scheduled task's connections are wired to the cluster's EC2 capacity security groups " +
|
|
276
|
+
"(EcsCluster.getEc2CapacityConnectable), which an awsvpc task's own ENI does not wear, so every " +
|
|
277
|
+
"connection grant would be unreachable at runtime with nothing failing at synth. Remove networkMode " +
|
|
278
|
+
"to run on the instance ENI, or set NetworkMode.HOST. Govern the task's reachability with the " +
|
|
279
|
+
"cluster-level securityGroup on EcsClusterConfig.");
|
|
280
|
+
}
|
|
281
|
+
if (entry.logGroup !== undefined && entry.logRetention !== undefined) {
|
|
282
|
+
throw new Error(`Scheduled task '${entry.name}': "logGroup" and "logRetention" are mutually exclusive on ` +
|
|
283
|
+
"EcsScheduledTaskConfig — the supplied logGroup owns its own retention, so logRetention " +
|
|
284
|
+
`(${entry.logRetention}) would be silently discarded. Supply one or the other, not both.`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
272
287
|
}
|
|
273
288
|
const DEFAULT_MIGRATE_CONTAINER_NAME = "migrate";
|
|
274
289
|
/**
|
|
@@ -976,6 +991,7 @@ export class EcsCompute extends Construct {
|
|
|
976
991
|
applicationId: props.applicationId ??
|
|
977
992
|
(App.hasInstance() ? App.getInstance().getName() : undefined),
|
|
978
993
|
...(props.taskStopWatchdog === false && { taskStopWatchdog: false }),
|
|
994
|
+
...(props.deployableServices === false && { deployableServices: false }),
|
|
979
995
|
...(props.containerInsights !== undefined && {
|
|
980
996
|
containerInsights: props.containerInsights
|
|
981
997
|
}),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Repository } from "aws-cdk-lib/aws-ecr";
|
|
2
2
|
import { type RepositoryImage, type ContainerImage, type PortMapping, type Secret as EcsSecret, type NetworkMode } from "aws-cdk-lib/aws-ecs";
|
|
3
|
-
import { type IPeer, type ISecurityGroup, type IVpc, type Port
|
|
3
|
+
import { type IPeer, type ISecurityGroup, type IVpc, type Port } from "aws-cdk-lib/aws-ec2";
|
|
4
4
|
import { type ILogGroup, type RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
5
5
|
import { type PolicyDocument, type IManagedPolicy, type PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
6
6
|
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
@@ -554,7 +554,8 @@ export interface EcsScheduledTaskConfig {
|
|
|
554
554
|
* Connections to data resources — the same connector interface services
|
|
555
555
|
* use. Produces IAM grants on the task role, security-group wiring (via the
|
|
556
556
|
* cluster's EC2 capacity security group — scheduled tasks run on the
|
|
557
|
-
* cluster's instances
|
|
557
|
+
* cluster's instances, which is why `networkMode: AWS_VPC` is refused
|
|
558
|
+
* here), and schema-version gate env injection when a
|
|
558
559
|
* connected database declares `migrations:`. Scheduled tasks never
|
|
559
560
|
* materialise a `fjall-schema-gate` container; only the env is injected —
|
|
560
561
|
* the task's own entrypoint decides whether to gate on it.
|
|
@@ -576,8 +577,18 @@ export interface EcsScheduledTaskConfig {
|
|
|
576
577
|
* removal default (D17).
|
|
577
578
|
*/
|
|
578
579
|
logRetention?: RetentionDays;
|
|
579
|
-
|
|
580
|
-
|
|
580
|
+
/**
|
|
581
|
+
* Task network mode. Omit for the `Ec2TaskDefinition` default (BRIDGE, as
|
|
582
|
+
* measured on aws-cdk-lib 2.265.0 — the caret range can move it); `HOST` is
|
|
583
|
+
* permitted.
|
|
584
|
+
*
|
|
585
|
+
* `AWS_VPC` is REFUSED at synth. A scheduled task's `connections` are wired
|
|
586
|
+
* onto the cluster's EC2 capacity security groups
|
|
587
|
+
* (`EcsCluster.getEc2CapacityConnectable`), which an awsvpc task's own ENI
|
|
588
|
+
* does not wear — every grant would land and every network path would not,
|
|
589
|
+
* with nothing failing at synth. Govern reachability with the cluster-level
|
|
590
|
+
* `securityGroup` on `EcsClusterConfig` instead.
|
|
591
|
+
*/
|
|
581
592
|
networkMode?: NetworkMode;
|
|
582
593
|
/** Host-bind volumes mounted into the task's container. */
|
|
583
594
|
volumes?: ContainerVolume[];
|
|
@@ -641,6 +652,10 @@ export interface EcsClusterConfig {
|
|
|
641
652
|
* Scheduled tasks materialised into `Ec2TaskDefinition`s and registered via
|
|
642
653
|
* `app.addSchedule(...)` against the existing `EcsScheduleTarget` shape
|
|
643
654
|
* ({ ecs, serviceName, taskCount }). See AC34.
|
|
655
|
+
*
|
|
656
|
+
* These run on the cluster's own EC2 instances and share their network
|
|
657
|
+
* interface, so `networkMode: AWS_VPC` is refused per entry — see
|
|
658
|
+
* {@link EcsScheduledTaskConfig.networkMode}.
|
|
644
659
|
*/
|
|
645
660
|
scheduledTasks?: EcsScheduledTaskConfig[];
|
|
646
661
|
}
|
|
@@ -1081,6 +1096,17 @@ export interface EcsComputeProps {
|
|
|
1081
1096
|
* opt-out for cost-sensitive clusters.
|
|
1082
1097
|
*/
|
|
1083
1098
|
taskStopWatchdog?: false;
|
|
1099
|
+
/**
|
|
1100
|
+
* Deployable-service exports (default on): one `DeployableService` output
|
|
1101
|
+
* per service plus the cluster's `DeployableCluster` and `ClusterArn`
|
|
1102
|
+
* outputs, named by `@fjall/util`'s `stackExportNames` builders. The
|
|
1103
|
+
* deploy engine's `EcsServiceResolver` reads the app's Compute stack
|
|
1104
|
+
* outputs to find the ECS services that `fjall services`, `fjall rollout`,
|
|
1105
|
+
* `fjall restart`, a code-only deploy and a rollback act on. `false` opts
|
|
1106
|
+
* out a cluster hosting an infrastructure service, so the resolver never
|
|
1107
|
+
* binds it as an app service.
|
|
1108
|
+
*/
|
|
1109
|
+
deployableServices?: false;
|
|
1084
1110
|
/**
|
|
1085
1111
|
* CloudWatch Container Insights (default off). Enabling it bills
|
|
1086
1112
|
* ~$0.30/metric-month of custom `ECS/ContainerInsights` metrics per cluster
|
|
@@ -28,6 +28,16 @@ export interface IQueueProps {
|
|
|
28
28
|
maxReceiveCount?: number;
|
|
29
29
|
};
|
|
30
30
|
encryption?: "SSE_SQS" | "SSE_KMS" | "NONE";
|
|
31
|
+
/**
|
|
32
|
+
* ARN of a customer-managed KMS key to encrypt the queue (and its
|
|
33
|
+
* auto-created DLQ) with. Requires `encryption: "SSE_KMS"` — the resource
|
|
34
|
+
* layer refuses the pair rather than ignoring the key. Absent with
|
|
35
|
+
* `"SSE_KMS"`, the queue takes the stack's shared key.
|
|
36
|
+
*
|
|
37
|
+
* An ARN rather than an `IKey` because this is the plan vocabulary the
|
|
38
|
+
* scaffold and the codemod write, the same as `S3Props.kmsKeyArn`; a key
|
|
39
|
+
* you hold in `infrastructure.ts` passes as `key.keyArn`.
|
|
40
|
+
*/
|
|
31
41
|
kmsKeyArn?: string;
|
|
32
42
|
contentBasedDeduplication?: boolean;
|
|
33
43
|
fifoThroughputLimit?: "perQueue" | "perMessageGroupId";
|