@fjall/components-infrastructure 38.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 +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 +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/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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Key } from "aws-cdk-lib/aws-kms";
|
|
1
2
|
import { EventBus as CdkEventBus } from "aws-cdk-lib/aws-events";
|
|
2
3
|
import { SQSQueue, SQS_LIMITS } from "../../resources/aws/messaging/sqs.js";
|
|
3
4
|
import { SNSTopic } from "../../resources/aws/messaging/sns.js";
|
|
@@ -49,11 +50,10 @@ function validateSQSProps(props) {
|
|
|
49
50
|
"This setting will be ignored for standard queues.");
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
53
|
+
// `kmsKeyArn` beside a non-KMS encryption used to warn here and then be
|
|
54
|
+
// ignored. `SQSQueue` refuses it instead — a warning that the queue is
|
|
55
|
+
// about to discard the key it was handed is not a gate, and the refusal
|
|
56
|
+
// states both cures.
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* Validates SNS Topic props.
|
|
@@ -85,7 +85,11 @@ export class QueueMessaging extends SQSQueue {
|
|
|
85
85
|
deliveryDelay: props.deliveryDelay,
|
|
86
86
|
deadLetterQueue: props.deadLetterQueue,
|
|
87
87
|
encryption: props.encryption,
|
|
88
|
-
|
|
88
|
+
// Resolved in the parent scope for the same reason `alertsTopic` is:
|
|
89
|
+
// `this` does not exist before super().
|
|
90
|
+
masterKey: props.kmsKeyArn === undefined
|
|
91
|
+
? undefined
|
|
92
|
+
: Key.fromKeyArn(scope, `${id}KmsKey`, props.kmsKeyArn),
|
|
89
93
|
contentBasedDeduplication: props.contentBasedDeduplication,
|
|
90
94
|
fifoThroughputLimit: props.fifoThroughputLimit,
|
|
91
95
|
deduplicationScope: props.deduplicationScope,
|
|
@@ -47,7 +47,35 @@ export interface S3Props {
|
|
|
47
47
|
* that versions after all is not stamped.
|
|
48
48
|
*/
|
|
49
49
|
readonly versioningExemption?: S3VersioningExemption;
|
|
50
|
+
/**
|
|
51
|
+
* `"AES256"` is SSE-S3: AWS's own key, free, and real encryption at rest.
|
|
52
|
+
* `"KMS"` is SSE-KMS with a key of YOUR OWN, so it requires `kmsKeyArn` —
|
|
53
|
+
* it is refused without one rather than falling back.
|
|
54
|
+
*
|
|
55
|
+
* The fallback is what this refuses. Asking CloudFormation for KMS
|
|
56
|
+
* encryption with no key makes CDK mint an `AWS::KMS::Key` of its own,
|
|
57
|
+
* `DeletionPolicy: Retain`, with no alias and no tags — an orphan every
|
|
58
|
+
* Fjall teardown sweep is blind to, because a sweep finds keys by the
|
|
59
|
+
* alias tag `CustomerManagedKey` stamps and that key carries none.
|
|
60
|
+
*
|
|
61
|
+
* Nor does the bucket quietly join the stack's shared key, the way an
|
|
62
|
+
* `"SSE_KMS"` queue does. A queue holds messages for at most fourteen
|
|
63
|
+
* days; a bucket holds objects forever, and an object is readable only
|
|
64
|
+
* with the key it was written under — so moving a live bucket's key
|
|
65
|
+
* revokes read access to everything already in it, at the same deploy
|
|
66
|
+
* that rewrites the reader's `kms:Decrypt` grant onto the new key. The
|
|
67
|
+
* choice of key for a bucket is the operator's, always.
|
|
68
|
+
*/
|
|
50
69
|
readonly encryption?: "AES256" | "KMS";
|
|
70
|
+
/**
|
|
71
|
+
* The customer managed key this bucket's objects are encrypted with.
|
|
72
|
+
* Implies `encryption: "KMS"` when no encryption is stated, and is refused
|
|
73
|
+
* beside `"AES256"` rather than silently ignored.
|
|
74
|
+
*
|
|
75
|
+
* An ARN rather than an `IKey` because this is the plan vocabulary the
|
|
76
|
+
* scaffold and the codemod write; a key you hold in `infrastructure.ts`
|
|
77
|
+
* passes as `key.keyArn`.
|
|
78
|
+
*/
|
|
51
79
|
readonly kmsKeyArn?: string;
|
|
52
80
|
/**
|
|
53
81
|
* S3 Bucket Keys for KMS-encrypted buckets — defaults to true when
|
|
@@ -94,6 +122,19 @@ export interface StorageBuildProps extends S3Props {
|
|
|
94
122
|
export type StorageFactoryFn = ((app: App, scope: Construct) => Storage) & {
|
|
95
123
|
stackPlacement?: "storage" | "cdn" | "compute";
|
|
96
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* Refuse the two `encryption`/`kmsKeyArn` pairings a bucket cannot honour.
|
|
127
|
+
*
|
|
128
|
+
* Both were warnings until 38.x, and both warnings said something untrue.
|
|
129
|
+
* `"KMS"` with no key did not fall back to "the default AWS managed key": it
|
|
130
|
+
* made CDK mint a retained, alias-less, tag-less key of its own beside a
|
|
131
|
+
* bucket that may be DESTROY — the orphan class this refusal exists for. And
|
|
132
|
+
* a key beside `"AES256"` was not "ignored": CDK refuses that pair itself,
|
|
133
|
+
* one frame later, in construct vocabulary the operator did not type.
|
|
134
|
+
*
|
|
135
|
+
* A key with NO stated encryption is neither, and is deliberately allowed —
|
|
136
|
+
* CDK infers KMS from the key, which is what the caller asked for.
|
|
137
|
+
*/
|
|
97
138
|
export declare function validateStorageProps(props: S3Props): void;
|
|
98
139
|
export declare class Storage extends Construct implements IStorage, IStorageConnector {
|
|
99
140
|
readonly connectorType: "storage";
|
|
@@ -4,7 +4,6 @@ import { Source, CacheControl } from "aws-cdk-lib/aws-s3-deployment";
|
|
|
4
4
|
import { Key } from "aws-cdk-lib/aws-kms";
|
|
5
5
|
import { CfnOutput, Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
|
|
6
6
|
import { BucketDeployment, S3Bucket } from "../../resources/aws/storage/index.js";
|
|
7
|
-
import { FjallLogger } from "../../utils/validationLogger.js";
|
|
8
7
|
export { isStorage } from "./interfaces/storage.js";
|
|
9
8
|
function toBucketEncryption(encryption) {
|
|
10
9
|
if (!encryption)
|
|
@@ -50,14 +49,30 @@ function resolveStorageRemovalPolicy(props) {
|
|
|
50
49
|
}
|
|
51
50
|
return props.retain ? RemovalPolicy.RETAIN : undefined;
|
|
52
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Refuse the two `encryption`/`kmsKeyArn` pairings a bucket cannot honour.
|
|
54
|
+
*
|
|
55
|
+
* Both were warnings until 38.x, and both warnings said something untrue.
|
|
56
|
+
* `"KMS"` with no key did not fall back to "the default AWS managed key": it
|
|
57
|
+
* made CDK mint a retained, alias-less, tag-less key of its own beside a
|
|
58
|
+
* bucket that may be DESTROY — the orphan class this refusal exists for. And
|
|
59
|
+
* a key beside `"AES256"` was not "ignored": CDK refuses that pair itself,
|
|
60
|
+
* one frame later, in construct vocabulary the operator did not type.
|
|
61
|
+
*
|
|
62
|
+
* A key with NO stated encryption is neither, and is deliberately allowed —
|
|
63
|
+
* CDK infers KMS from the key, which is what the caller asked for.
|
|
64
|
+
*/
|
|
53
65
|
export function validateStorageProps(props) {
|
|
54
|
-
if (props.encryption === "KMS" &&
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
if (props.encryption === "KMS" && props.kmsKeyArn === undefined) {
|
|
67
|
+
throw new Error(`encryption: "KMS" needs the ARN of the key to encrypt with. Pass ` +
|
|
68
|
+
`'kmsKeyArn', or use encryption: "AES256" for S3-managed encryption ` +
|
|
69
|
+
`at rest, which costs nothing and needs no key.`);
|
|
70
|
+
}
|
|
71
|
+
if (props.kmsKeyArn !== undefined && props.encryption === "AES256") {
|
|
72
|
+
throw new Error(`'kmsKeyArn' encrypts with a key of your own and encryption: "AES256" ` +
|
|
73
|
+
`encrypts with S3's, so the pair has no meaning. Set ` +
|
|
74
|
+
`encryption: "KMS" to use the key, or drop 'kmsKeyArn' to keep ` +
|
|
75
|
+
`"AES256".`);
|
|
61
76
|
}
|
|
62
77
|
}
|
|
63
78
|
export class Storage extends Construct {
|
|
@@ -29,6 +29,16 @@ export declare class BackupVault extends Construct {
|
|
|
29
29
|
readonly vault: Vault;
|
|
30
30
|
readonly vaultArn: CfnOutput;
|
|
31
31
|
readonly vaultName: CfnOutput;
|
|
32
|
+
/** The key the vault encrypts recovery points with. */
|
|
33
|
+
readonly encryptionKey: IKey;
|
|
34
|
+
/**
|
|
35
|
+
* Whether this stack created the key. A re-owned vault names a live key the
|
|
36
|
+
* stack does not manage, and `addToResourcePolicy` on an imported key is a
|
|
37
|
+
* no-op — so a caller that needs a statement on the key policy (a
|
|
38
|
+
* cross-account copy destination does) must check this and say so when it
|
|
39
|
+
* cannot apply one.
|
|
40
|
+
*/
|
|
41
|
+
readonly ownsEncryptionKey: boolean;
|
|
32
42
|
constructor(scope: Construct, id: string, props: BackupVaultProps);
|
|
33
43
|
static build(id: string, props: BackupVaultProps): (scope: Construct) => BackupVault;
|
|
34
44
|
}
|
|
@@ -18,6 +18,16 @@ export class BackupVault extends Construct {
|
|
|
18
18
|
vault;
|
|
19
19
|
vaultArn;
|
|
20
20
|
vaultName;
|
|
21
|
+
/** The key the vault encrypts recovery points with. */
|
|
22
|
+
encryptionKey;
|
|
23
|
+
/**
|
|
24
|
+
* Whether this stack created the key. A re-owned vault names a live key the
|
|
25
|
+
* stack does not manage, and `addToResourcePolicy` on an imported key is a
|
|
26
|
+
* no-op — so a caller that needs a statement on the key policy (a
|
|
27
|
+
* cross-account copy destination does) must check this and say so when it
|
|
28
|
+
* cannot apply one.
|
|
29
|
+
*/
|
|
30
|
+
ownsEncryptionKey;
|
|
21
31
|
constructor(scope, id, props) {
|
|
22
32
|
super(scope, id);
|
|
23
33
|
// RETAIN default: a DESTROY-policy vault is silently deleted with the stack.
|
|
@@ -41,6 +51,8 @@ export class BackupVault extends Construct {
|
|
|
41
51
|
removalPolicy
|
|
42
52
|
}).key;
|
|
43
53
|
}
|
|
54
|
+
this.encryptionKey = encryptionKey;
|
|
55
|
+
this.ownsEncryptionKey = props.encryptionKey === undefined;
|
|
44
56
|
this.vault = new Vault(this, `${props.vaultName}Vault`, {
|
|
45
57
|
backupVaultName: props.vaultName,
|
|
46
58
|
encryptionKey,
|
|
@@ -152,11 +152,14 @@ export default class EcsCluster extends Construct implements IConnectable {
|
|
|
152
152
|
private addServiceToCluster;
|
|
153
153
|
private setupConnections;
|
|
154
154
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
155
|
+
* The outputs the deploy engine's `EcsServiceResolver` reads to find this
|
|
156
|
+
* cluster's services: the cluster's `DeployableCluster` and `ClusterArn`,
|
|
157
|
+
* then one `DeployableService` per service. Export names come from
|
|
158
|
+
* `@fjall/util`'s `stackExportNames` builders — the same functions the
|
|
159
|
+
* resolver rebuilds them with — and the output keys keep the PascalCase
|
|
160
|
+
* form a logical id needs.
|
|
158
161
|
*/
|
|
159
|
-
private
|
|
162
|
+
private addDeployableOutputs;
|
|
160
163
|
/** Checks if any service in the cluster uses a Fargate capacity provider. */
|
|
161
164
|
private anyServiceUsesFargate;
|
|
162
165
|
/** Checks if any service in the cluster uses an EC2 capacity provider. */
|
|
@@ -8,7 +8,7 @@ import { tierDefault } from "../../../utils/backupTierMapping.js";
|
|
|
8
8
|
import { envAwareRemovalPolicyDefault } from "../../../utils/removalPolicy.js";
|
|
9
9
|
import { StackEncryptionKey } from "../secrets/stackEncryptionKey.js";
|
|
10
10
|
import { DEFAULT_FRAMEWORK_LOG_RETENTION, LogGroup } from "../logging/logGroup.js";
|
|
11
|
-
import {
|
|
11
|
+
import { clusterArnExportName, deployableClusterExportName, deployableServiceExportName } from "@fjall/util";
|
|
12
12
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
13
13
|
import { createEcsServiceAlarms, createEcsTaskStopWatchdog, createLogPatternAlarms } from "../monitoring/index.js";
|
|
14
14
|
// Extracted modules
|
|
@@ -177,7 +177,9 @@ export default class EcsCluster extends Construct {
|
|
|
177
177
|
applicationId: props.applicationId
|
|
178
178
|
});
|
|
179
179
|
}
|
|
180
|
-
|
|
180
|
+
if (props.deployableServices !== false) {
|
|
181
|
+
this.addDeployableOutputs(props);
|
|
182
|
+
}
|
|
181
183
|
this.connections = this.setupConnections(props);
|
|
182
184
|
Aspects.of(this).add(new CapacityProviderDependencyAspect(this.cluster));
|
|
183
185
|
}
|
|
@@ -412,16 +414,31 @@ export default class EcsCluster extends Construct {
|
|
|
412
414
|
});
|
|
413
415
|
}
|
|
414
416
|
/**
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
417
|
+
* The outputs the deploy engine's `EcsServiceResolver` reads to find this
|
|
418
|
+
* cluster's services: the cluster's `DeployableCluster` and `ClusterArn`,
|
|
419
|
+
* then one `DeployableService` per service. Export names come from
|
|
420
|
+
* `@fjall/util`'s `stackExportNames` builders — the same functions the
|
|
421
|
+
* resolver rebuilds them with — and the output keys keep the PascalCase
|
|
422
|
+
* form a logical id needs.
|
|
418
423
|
*/
|
|
419
|
-
|
|
424
|
+
addDeployableOutputs(props) {
|
|
425
|
+
const stackName = Stack.of(this).stackName;
|
|
426
|
+
new CfnOutput(this, `${this.outputName}DeployableCluster`, {
|
|
427
|
+
key: `${this.outputName}DeployableCluster`,
|
|
428
|
+
exportName: deployableClusterExportName(stackName, props.clusterName),
|
|
429
|
+
value: this.cluster.clusterArn
|
|
430
|
+
});
|
|
431
|
+
new CfnOutput(this, `${this.outputName}ClusterArn`, {
|
|
432
|
+
key: `${this.outputName}ClusterArn`,
|
|
433
|
+
exportName: clusterArnExportName(stackName, props.clusterName),
|
|
434
|
+
value: this.cluster.clusterArn,
|
|
435
|
+
description: `ECS Cluster ARN for ${props.clusterName}`
|
|
436
|
+
});
|
|
420
437
|
for (const [serviceName, serviceData] of this.services) {
|
|
421
|
-
const
|
|
422
|
-
new CfnOutput(this,
|
|
423
|
-
key:
|
|
424
|
-
exportName:
|
|
438
|
+
const outputKey = `${this.outputName}${toPascalCase(serviceName)}DeployableService`;
|
|
439
|
+
new CfnOutput(this, outputKey, {
|
|
440
|
+
key: outputKey,
|
|
441
|
+
exportName: deployableServiceExportName(stackName, props.clusterName, serviceName),
|
|
425
442
|
value: serviceData.service.serviceArn,
|
|
426
443
|
description: `Deployable ECS Service ARN for ${serviceName} in ${props.clusterName}`
|
|
427
444
|
});
|
|
@@ -467,17 +484,6 @@ export default class EcsCluster extends Construct {
|
|
|
467
484
|
}
|
|
468
485
|
}
|
|
469
486
|
});
|
|
470
|
-
new CfnOutput(this, `${this.outputName}DeployableCluster`, {
|
|
471
|
-
key: `${this.outputName}DeployableCluster`,
|
|
472
|
-
exportName: stackScopedExportName(this, `${props.clusterName}DeployableCluster`),
|
|
473
|
-
value: cluster.clusterArn
|
|
474
|
-
});
|
|
475
|
-
new CfnOutput(this, `${this.outputName}ClusterArn`, {
|
|
476
|
-
key: `${this.outputName}ClusterArn`,
|
|
477
|
-
exportName: stackScopedExportName(this, `${props.clusterName}ClusterArn`),
|
|
478
|
-
value: cluster.clusterArn,
|
|
479
|
-
description: `ECS Cluster ARN for ${props.clusterName}`
|
|
480
|
-
});
|
|
481
487
|
return cluster;
|
|
482
488
|
}
|
|
483
489
|
static build(id, props) {
|
|
@@ -195,6 +195,9 @@ export function getOrCreateAsgCapacityProvider(ctx, serviceProps, state) {
|
|
|
195
195
|
...(ec2Config.persistentDataVolume.alarmTopic === undefined &&
|
|
196
196
|
ctx.props.alertsTopic !== undefined && {
|
|
197
197
|
alarmTopic: ctx.props.alertsTopic
|
|
198
|
+
}),
|
|
199
|
+
...(ctx.props.appBackupTier !== undefined && {
|
|
200
|
+
appBackupTier: ctx.props.appBackupTier
|
|
198
201
|
})
|
|
199
202
|
},
|
|
200
203
|
// Pinned slots reproduce the deployed fjall:OwnerLogicalId verbatim —
|
|
@@ -763,6 +763,17 @@ export interface EcsClusterProps {
|
|
|
763
763
|
* auditable opt-out for cost-sensitive clusters.
|
|
764
764
|
*/
|
|
765
765
|
taskStopWatchdog?: false;
|
|
766
|
+
/**
|
|
767
|
+
* Deployable-service exports (default on): one `DeployableService` output
|
|
768
|
+
* per service plus the cluster's `DeployableCluster` and `ClusterArn`
|
|
769
|
+
* outputs, named by `@fjall/util`'s `stackExportNames` builders. The
|
|
770
|
+
* deploy engine's `EcsServiceResolver` reads the app's Compute stack
|
|
771
|
+
* outputs to find the ECS services that `fjall services`, `fjall rollout`,
|
|
772
|
+
* `fjall restart`, a code-only deploy and a rollback act on. `false` opts
|
|
773
|
+
* out a cluster hosting an infrastructure service, so the resolver never
|
|
774
|
+
* binds it as an app service.
|
|
775
|
+
*/
|
|
776
|
+
deployableServices?: false;
|
|
766
777
|
/**
|
|
767
778
|
* CloudWatch Container Insights. Default: the app backup tier's
|
|
768
779
|
* `containerInsights` column (on at enterprise, off below). Enabling it
|
|
@@ -26,6 +26,14 @@ import type { DomainConfig, EcsCapacityProvider, EcsClusterProps } from "./ecsTy
|
|
|
26
26
|
* a field on `EcsServiceProps`, so a direct `new EcsCluster(...)` consumer
|
|
27
27
|
* cannot pass it and there is no resources-layer code path to validate.
|
|
28
28
|
*
|
|
29
|
+
* Same applies to `cluster.scheduledTasks`: the whole array is a patterns-
|
|
30
|
+
* layer field on `EcsClusterConfig`, materialised into `Ec2TaskDefinition`s
|
|
31
|
+
* and `app.addSchedule(...)` calls by `materialiseScheduledTasks` in
|
|
32
|
+
* `computeEcs.ts`. Its two per-entry refusals (`networkMode: AWS_VPC`, and
|
|
33
|
+
* `logGroup` together with `logRetention`) therefore run in `validateEcsProps`
|
|
34
|
+
* only. `EcsClusterProps` carries no `scheduledTasks` field, so a mirror here
|
|
35
|
+
* would be unreachable.
|
|
36
|
+
*
|
|
29
37
|
* @param props - The cluster props to validate
|
|
30
38
|
* @param annotationsScope - When provided (the `EcsCluster` constructor passes
|
|
31
39
|
* `this`), non-fatal findings from {@link validateEc2TaskMemoryFit} are
|
|
@@ -32,6 +32,14 @@ import { validateSharedEc2CapacityConfig } from "./ecsCapacityConfig.js";
|
|
|
32
32
|
* a field on `EcsServiceProps`, so a direct `new EcsCluster(...)` consumer
|
|
33
33
|
* cannot pass it and there is no resources-layer code path to validate.
|
|
34
34
|
*
|
|
35
|
+
* Same applies to `cluster.scheduledTasks`: the whole array is a patterns-
|
|
36
|
+
* layer field on `EcsClusterConfig`, materialised into `Ec2TaskDefinition`s
|
|
37
|
+
* and `app.addSchedule(...)` calls by `materialiseScheduledTasks` in
|
|
38
|
+
* `computeEcs.ts`. Its two per-entry refusals (`networkMode: AWS_VPC`, and
|
|
39
|
+
* `logGroup` together with `logRetention`) therefore run in `validateEcsProps`
|
|
40
|
+
* only. `EcsClusterProps` carries no `scheduledTasks` field, so a mirror here
|
|
41
|
+
* would be unreachable.
|
|
42
|
+
*
|
|
35
43
|
* @param props - The cluster props to validate
|
|
36
44
|
* @param annotationsScope - When provided (the `EcsCluster` constructor passes
|
|
37
45
|
* `this`), non-fatal findings from {@link validateEc2TaskMemoryFit} are
|
|
@@ -3,6 +3,7 @@ import { Alarm } from "aws-cdk-lib/aws-cloudwatch";
|
|
|
3
3
|
import { EbsDeviceVolumeType, Volume } from "aws-cdk-lib/aws-ec2";
|
|
4
4
|
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
5
5
|
import { Construct } from "constructs";
|
|
6
|
+
import { type BackupTier } from "../../../utils/backupTierMapping.js";
|
|
6
7
|
import { LambdaFunction } from "./lambda.js";
|
|
7
8
|
import { SQSQueue } from "../messaging/sqs.js";
|
|
8
9
|
/**
|
|
@@ -53,6 +54,13 @@ export interface PersistentDataVolumeProps {
|
|
|
53
54
|
* re-attach queue's DLQ-depth alarm (created only when the topic is set).
|
|
54
55
|
*/
|
|
55
56
|
alarmTopic?: ITopic;
|
|
57
|
+
/**
|
|
58
|
+
* The application's backup tier. Enrols the volume in the matching AWS
|
|
59
|
+
* Backup plan via the disaster-recovery tier tag. Independent of the
|
|
60
|
+
* volume's `SNAPSHOT` removal policy, which only covers a clean destroy:
|
|
61
|
+
* the plan's recovery points are the copies that reach the DR region.
|
|
62
|
+
*/
|
|
63
|
+
appBackupTier?: BackupTier;
|
|
56
64
|
}
|
|
57
65
|
/**
|
|
58
66
|
* Standalone EBS data volume + ASG `EC2_INSTANCE_LAUNCHING` hook that
|
|
@@ -10,6 +10,7 @@ import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
|
10
10
|
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
11
11
|
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
12
12
|
import { Construct } from "constructs";
|
|
13
|
+
import { tagBackupTier } from "../../../utils/backupTierMapping.js";
|
|
13
14
|
import { attachInlineAsgLifecycleHook } from "./asgInlineLifecycleHook.js";
|
|
14
15
|
import { LambdaFunction } from "./lambda.js";
|
|
15
16
|
import { SQSQueue } from "../messaging/sqs.js";
|
|
@@ -104,6 +105,7 @@ export class PersistentDataVolume extends Construct {
|
|
|
104
105
|
Tags.of(this.volume).add(PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE, PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE_VALUE);
|
|
105
106
|
Tags.of(this.volume).add(PERSISTENT_DATA_VOLUME_TAG_OWNER_LOGICAL_ID, this.ownerLogicalId);
|
|
106
107
|
Tags.of(this.volume).add(PERSISTENT_DATA_VOLUME_TAG_STACK_ID, Aws.STACK_ID);
|
|
108
|
+
tagBackupTier(this.volume, props.appBackupTier);
|
|
107
109
|
this.queue = new SQSQueue(this, `${id}Queue`, {
|
|
108
110
|
visibilityTimeout: QUEUE_VISIBILITY_TIMEOUT_SECONDS,
|
|
109
111
|
deadLetterQueue: { enabled: true, maxReceiveCount: 5 },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { CfnOutput } from "aws-cdk-lib";
|
|
3
|
-
import { tierDefault } from "../../../utils/backupTierMapping.js";
|
|
3
|
+
import { tagBackupTier, tierDefault } from "../../../utils/backupTierMapping.js";
|
|
4
4
|
import { toRemovalPolicy } from "../../../utils/removalPolicy.js";
|
|
5
5
|
import { StackEncryptionKey } from "../secrets/stackEncryptionKey.js";
|
|
6
6
|
import { Table, AttributeType, BillingMode, StreamViewType, ProjectionType, TableEncryption } from "aws-cdk-lib/aws-dynamodb";
|
|
@@ -98,6 +98,7 @@ export class DynamoDBTable extends Construct {
|
|
|
98
98
|
removalPolicy: toRemovalPolicy(props.removalPolicy),
|
|
99
99
|
timeToLiveAttribute: props.timeToLiveAttribute
|
|
100
100
|
});
|
|
101
|
+
tagBackupTier(this.table, props.appBackupTier);
|
|
101
102
|
if (props.globalSecondaryIndexes) {
|
|
102
103
|
for (const gsi of props.globalSecondaryIndexes) {
|
|
103
104
|
const gsiProps = {
|
|
@@ -6,7 +6,7 @@ import { Construct } from "constructs";
|
|
|
6
6
|
import { SecurityGroup } from "../networking/securityGroup.js";
|
|
7
7
|
import { Secret } from "../secrets/index.js";
|
|
8
8
|
import { ResourceNaming } from "../../../utils/resourceNaming.js";
|
|
9
|
-
import { tierDefault } from "../../../utils/backupTierMapping.js";
|
|
9
|
+
import { tagBackupTier, tierDefault } from "../../../utils/backupTierMapping.js";
|
|
10
10
|
import { addProxyCfnOutput } from "./rdsProxyOutput.js";
|
|
11
11
|
import { getCdkDatabaseInsightsMode, getDatabaseInsightsRetention } from "./index.js";
|
|
12
12
|
import { RDS_DEFAULTS, SERVERLESS_V2_AUTO_PAUSE_MIN_SECONDS, SERVERLESS_V2_AUTO_PAUSE_MAX_SECONDS, MIN_AUTO_PAUSE_POSTGRES_VERSION } from "./rdsDefaults.js";
|
|
@@ -255,6 +255,11 @@ export class RdsAurora extends Construct {
|
|
|
255
255
|
this.databaseCluster = new DatabaseCluster(this, `${id}Database`, clusterProps);
|
|
256
256
|
}
|
|
257
257
|
this.cfnCluster = this.databaseCluster.node.defaultChild;
|
|
258
|
+
// The L1, not the L2: AWS Backup protects the Aurora cluster, and a tag on
|
|
259
|
+
// the DatabaseCluster construct would propagate to every member instance,
|
|
260
|
+
// each of which would then be selected as an RDS instance it cannot back
|
|
261
|
+
// up.
|
|
262
|
+
tagBackupTier(this.cfnCluster, props.appBackupTier);
|
|
258
263
|
// Global secondary clusters can't specify these properties
|
|
259
264
|
if (props.isGlobalSecondary && this.cfnCluster) {
|
|
260
265
|
this.cfnCluster.masterUsername = undefined;
|
|
@@ -13,7 +13,7 @@ import { RDS_DEFAULTS } from "./rdsDefaults.js";
|
|
|
13
13
|
import { DEFAULT_POSTGRES_ENGINE_CONFIG, resolveDatabaseInsights, resolveStorageEncryptionKey, resolvePerformanceInsightsKey, addMultiUserSecretRotation, warnIfSnapshotUsernameAssumed } from "./rdsHelpers.js";
|
|
14
14
|
import { resolveSecretRotation, isCMKRequested } from "../../../utils/databaseTypes.js";
|
|
15
15
|
import { ResourceNaming } from "../../../utils/resourceNaming.js";
|
|
16
|
-
import { tierDefault } from "../../../utils/backupTierMapping.js";
|
|
16
|
+
import { tagBackupTier, tierDefault } from "../../../utils/backupTierMapping.js";
|
|
17
17
|
import { addProxyCfnOutput } from "./rdsProxyOutput.js";
|
|
18
18
|
import { createRdsAlarms } from "../monitoring/index.js";
|
|
19
19
|
export class RdsInstance extends Construct {
|
|
@@ -189,6 +189,11 @@ export class RdsInstance extends Construct {
|
|
|
189
189
|
credentials: Credentials.fromSecret(this.databaseCredentials.secret)
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
|
+
// The L1, not the L2: a tag on the DatabaseInstance construct propagates
|
|
193
|
+
// to the subnet group it creates. The read replica added below is
|
|
194
|
+
// deliberately left untagged — AWS Backup would protect it as a second
|
|
195
|
+
// full copy of storage the primary's own recovery points already hold.
|
|
196
|
+
tagBackupTier(this.database.node.defaultChild, props.appBackupTier);
|
|
192
197
|
}
|
|
193
198
|
rotateSecret(rotationConfig) {
|
|
194
199
|
this.masterSecret = addMultiUserSecretRotation({
|
|
@@ -21,7 +21,7 @@ export declare const ACCESS_LOG_RETENTION_DAYS = 90;
|
|
|
21
21
|
* `open`. The bucket takes the env-aware removal policy like every other
|
|
22
22
|
* bucket: production RETAINs its audit evidence, non-production DESTROYs.
|
|
23
23
|
*
|
|
24
|
-
* Migration from 33.0.0–
|
|
24
|
+
* Migration from 33.0.0–37.x: CloudFront standard (legacy) logging granted
|
|
25
25
|
* its `awslogsdelivery` canonical user FULL_CONTROL on the bucket ACL and
|
|
26
26
|
* never removes it, so on a bucket legacy logging wrote to the ownership
|
|
27
27
|
* flip fails with `InvalidBucketAclWithObjectOwnership`. Cure once, then
|
|
@@ -23,7 +23,7 @@ export const ACCESS_LOG_RETENTION_DAYS = 90;
|
|
|
23
23
|
* `open`. The bucket takes the env-aware removal policy like every other
|
|
24
24
|
* bucket: production RETAINs its audit evidence, non-production DESTROYs.
|
|
25
25
|
*
|
|
26
|
-
* Migration from 33.0.0–
|
|
26
|
+
* Migration from 33.0.0–37.x: CloudFront standard (legacy) logging granted
|
|
27
27
|
* its `awslogsdelivery` canonical user FULL_CONTROL on the bucket ACL and
|
|
28
28
|
* never removes it, so on a bucket legacy logging wrote to the ownership
|
|
29
29
|
* flip fails with `InvalidBucketAclWithObjectOwnership`. Cure once, then
|
|
@@ -42,6 +42,9 @@ export class Trail extends Construct {
|
|
|
42
42
|
encryptionKey: this.encryptionKey.key,
|
|
43
43
|
versioned: true,
|
|
44
44
|
removalPolicy: storagePolicy,
|
|
45
|
+
// CloudTrail delivers here continuously under the 365-day rule below;
|
|
46
|
+
// the audit record is the trail, not a restorable application state.
|
|
47
|
+
backupExemption: "log-delivery",
|
|
45
48
|
// Custom lifecycleRules bypass S3Bucket's automatic
|
|
46
49
|
// noncurrentVersionExpiration pairing — carried explicitly, or
|
|
47
50
|
// noncurrent versions accumulate forever.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type IRuleRef } from "aws-cdk-lib/aws-events";
|
|
2
2
|
import type { IQueue } from "aws-cdk-lib/aws-sqs";
|
|
3
|
+
/** EventBridge's service principal — the sender every grant in this module admits. */
|
|
4
|
+
export declare const EVENTBRIDGE_SERVICE_PRINCIPAL = "events.amazonaws.com";
|
|
3
5
|
/**
|
|
4
6
|
* Let `rule` deliver to `queue`. Not for a queue encrypted with a customer
|
|
5
7
|
* managed key: naming the rule there closes a dependency cycle through the
|
|
@@ -2,7 +2,8 @@ import { Construct } from "constructs";
|
|
|
2
2
|
import { Annotations, Lazy, Names, Token, TokenComparison } from "aws-cdk-lib";
|
|
3
3
|
import { CfnRule } from "aws-cdk-lib/aws-events";
|
|
4
4
|
import { PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
5
|
-
|
|
5
|
+
/** EventBridge's service principal — the sender every grant in this module admits. */
|
|
6
|
+
export const EVENTBRIDGE_SERVICE_PRINCIPAL = "events.amazonaws.com";
|
|
6
7
|
/** The actions `IQueue.grantSendMessages` grants — what CDK's SQS rule target allows. */
|
|
7
8
|
const RULE_TARGET_ACTIONS = [
|
|
8
9
|
"sqs:SendMessage",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
|
-
import { Duration } from "aws-cdk-lib";
|
|
2
|
+
import { Annotations, Duration } from "aws-cdk-lib";
|
|
3
3
|
import { Rule } from "aws-cdk-lib/aws-events";
|
|
4
|
-
import { ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
4
|
+
import { PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
5
5
|
import { SQSQueue, SQS_LIMITS } from "./sqs.js";
|
|
6
6
|
import { resolveTarget } from "./eventTargets.js";
|
|
7
|
+
import { EVENTBRIDGE_SERVICE_PRINCIPAL } from "./eventBridgeQueueGrants.js";
|
|
7
8
|
import { createScheduleAlarms } from "../monitoring/scheduleAlarms.js";
|
|
8
9
|
import { createSqsDlqAlarms } from "../monitoring/sqsAlarms.js";
|
|
9
10
|
/**
|
|
@@ -36,7 +37,7 @@ export class EventBridgeRule extends Construct {
|
|
|
36
37
|
const dlq = dlqResolution?.queue;
|
|
37
38
|
if (dlq !== undefined)
|
|
38
39
|
this.deadLetterQueue = dlq;
|
|
39
|
-
grantKmsDecryptToEventBridge(dlq);
|
|
40
|
+
grantKmsDecryptToEventBridge(this, dlq);
|
|
40
41
|
const retryPolicy = resolveRetryPolicy(props.kind, props.retryPolicy);
|
|
41
42
|
const targetAdapter = resolveTarget(props.target, {
|
|
42
43
|
retryPolicy,
|
|
@@ -84,13 +85,43 @@ export class EventBridgeRule extends Construct {
|
|
|
84
85
|
return this.#rule.ruleArn;
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Actions EventBridge needs on a dead-letter queue's key to encrypt the
|
|
90
|
+
* event it is dropping there.
|
|
91
|
+
*/
|
|
92
|
+
const EVENTBRIDGE_DLQ_KEY_ACTIONS = ["kms:Decrypt", "kms:GenerateDataKey"];
|
|
93
|
+
/**
|
|
94
|
+
* Admit EventBridge to a CMK-encrypted dead-letter queue's key.
|
|
95
|
+
*
|
|
96
|
+
* Written as an `addToResourcePolicy` rather than `IKey.grant` because only
|
|
97
|
+
* the former reports whether the statement landed. A key imported by ARN —
|
|
98
|
+
* what `kmsKeyArn` on the patterns layer produces — carries no policy
|
|
99
|
+
* document CDK can write, and `grant` returns a Grant whose `success` is
|
|
100
|
+
* `true` regardless, so the admission would be lost in silence. It is the
|
|
101
|
+
* worst place to lose one: SQS DROPS a send its key refuses instead of
|
|
102
|
+
* returning an error, so the failure surfaces only as a dead letter that
|
|
103
|
+
* never arrives, long after the deploy that caused it.
|
|
104
|
+
*
|
|
105
|
+
* The warning therefore names the statement the key's owner must add. Fjall
|
|
106
|
+
* cannot add it: the key is theirs, and it may not even be in this account.
|
|
107
|
+
*/
|
|
108
|
+
function grantKmsDecryptToEventBridge(scope, dlq) {
|
|
88
109
|
if (dlq === undefined)
|
|
89
110
|
return;
|
|
90
111
|
const key = dlq.getQueue().encryptionMasterKey;
|
|
91
112
|
if (key === undefined)
|
|
92
113
|
return;
|
|
93
|
-
key.
|
|
114
|
+
const admitted = key.addToResourcePolicy(new PolicyStatement({
|
|
115
|
+
principals: [new ServicePrincipal(EVENTBRIDGE_SERVICE_PRINCIPAL)],
|
|
116
|
+
actions: EVENTBRIDGE_DLQ_KEY_ACTIONS,
|
|
117
|
+
resources: ["*"]
|
|
118
|
+
})).statementAdded;
|
|
119
|
+
if (admitted)
|
|
120
|
+
return;
|
|
121
|
+
Annotations.of(scope).addWarningV2("@fjall/components-infrastructure:eventBridgeDeadLetterKeyNotAdmitted", `The dead-letter queue's KMS key is imported, so this stack cannot add ` +
|
|
122
|
+
`to its key policy. Add ${EVENTBRIDGE_DLQ_KEY_ACTIONS.join(" + ")} for ` +
|
|
123
|
+
`${EVENTBRIDGE_SERVICE_PRINCIPAL} to the key yourself, or SQS will ` +
|
|
124
|
+
`silently drop every event EventBridge dead-letters to this queue.`);
|
|
94
125
|
}
|
|
95
126
|
function resolveRetryPolicy(kind, override) {
|
|
96
127
|
if (override !== undefined)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { type IQueue } from "aws-cdk-lib/aws-sqs";
|
|
3
3
|
import { type IGrantable, type Grant } from "aws-cdk-lib/aws-iam";
|
|
4
|
+
import { type IKey } from "aws-cdk-lib/aws-kms";
|
|
4
5
|
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
5
6
|
import { type RemovalPolicyString } from "./utils.js";
|
|
6
7
|
import { type SqsAlarmThresholds } from "../monitoring/index.js";
|
|
@@ -57,8 +58,42 @@ export interface SQSQueueProps {
|
|
|
57
58
|
maxReceiveCount?: number;
|
|
58
59
|
queue?: IQueue;
|
|
59
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Server-side encryption at rest. Defaults to `"SSE_SQS"` — the SQS-owned
|
|
63
|
+
* managed key, which is free, needs no key policy and satisfies
|
|
64
|
+
* `sqs-queue-not-encrypted`.
|
|
65
|
+
*
|
|
66
|
+
* `"SSE_KMS"` encrypts with a customer-managed key: `masterKey` when you
|
|
67
|
+
* bring one, else the stack's shared {@link StackEncryptionKey} with the
|
|
68
|
+
* SQS senders admitted to it. It deliberately does NOT fall through to
|
|
69
|
+
* what CDK would do unaided — mint an implicit `AWS::KMS::Key` that is
|
|
70
|
+
* `DeletionPolicy: Retain` with no alias and no tags, admitting only the
|
|
71
|
+
* account root. Such a key is invisible to every Fjall teardown sweep
|
|
72
|
+
* (they find keys by the alias tag `CustomerManagedKey` stamps), so it
|
|
73
|
+
* outlives the stack as a charge nothing can attribute — and because its
|
|
74
|
+
* policy admits no service principal, no EventBridge rule, S3 notification
|
|
75
|
+
* or SNS subscription could send to the queue either.
|
|
76
|
+
*
|
|
77
|
+
* Unlike SNS, this does not follow the app's backup tier. `"SSE_SQS"` is
|
|
78
|
+
* real encryption at rest at every tier and costs nothing, and the tier's
|
|
79
|
+
* `customerManagedKeys` column enumerates the resources it owns (DynamoDB
|
|
80
|
+
* tables, SNS topics, Lambda environment, ECS Exec, log groups) — queues
|
|
81
|
+
* are not among them, so the opt-in here is the caller's.
|
|
82
|
+
*/
|
|
60
83
|
encryption?: "SSE_SQS" | "SSE_KMS" | "NONE";
|
|
61
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Bring your own KMS key. Implies `encryption: "SSE_KMS"` when no
|
|
86
|
+
* encryption is stated, and is REFUSED beside `"SSE_SQS"` or `"NONE"`
|
|
87
|
+
* rather than silently ignored.
|
|
88
|
+
*
|
|
89
|
+
* Every AWS service principal that sends to the queue needs `kms:Decrypt`
|
|
90
|
+
* + `kms:GenerateDataKey*` on this key's RESOURCE policy or its sends are
|
|
91
|
+
* dropped without an error — the key policy is the only place a service
|
|
92
|
+
* principal can be admitted. The stack key's own admission
|
|
93
|
+
* ({@link StackEncryptionKey.forSqsServicePublishers}) is not applied to a
|
|
94
|
+
* key you brought: it is yours to scope.
|
|
95
|
+
*/
|
|
96
|
+
masterKey?: IKey;
|
|
62
97
|
contentBasedDeduplication?: boolean;
|
|
63
98
|
fifoThroughputLimit?: "perQueue" | "perMessageGroupId";
|
|
64
99
|
deduplicationScope?: "queue" | "messageGroup";
|