@fjall/components-infrastructure 27.0.0 → 28.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.js +8 -1
- package/dist/lib/config/aws/alarmTopic.js +20 -1
- package/dist/lib/config/aws/configRecorder.js +16 -5
- package/dist/lib/patterns/aws/computeLambda.d.ts +4 -1
- package/dist/lib/patterns/aws/computeLambda.js +4 -1
- package/dist/lib/patterns/aws/database.d.ts +11 -6
- package/dist/lib/patterns/aws/database.js +12 -3
- package/dist/lib/patterns/aws/interfaces/database.d.ts +6 -3
- package/dist/lib/patterns/aws/storage.d.ts +6 -0
- package/dist/lib/patterns/aws/storage.js +3 -0
- package/dist/lib/resources/aws/compute/applicationLoadBalancer.js +3 -0
- package/dist/lib/resources/aws/compute/ecs.js +11 -2
- package/dist/lib/resources/aws/compute/lambda.d.ts +46 -0
- package/dist/lib/resources/aws/compute/lambda.js +100 -2
- package/dist/lib/resources/aws/compute/listenerRouting.js +6 -2
- package/dist/lib/resources/aws/database/dynamodb.d.ts +1 -0
- package/dist/lib/resources/aws/database/dynamodb.js +14 -1
- package/dist/lib/resources/aws/database/rdsAurora.d.ts +8 -0
- package/dist/lib/resources/aws/database/rdsAurora.js +18 -1
- package/dist/lib/resources/aws/database/rdsAuroraGlobal.d.ts +7 -0
- package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +15 -0
- package/dist/lib/resources/aws/database/rdsInstance.d.ts +15 -7
- package/dist/lib/resources/aws/database/rdsInstance.js +21 -6
- package/dist/lib/resources/aws/logging/cloudTrail.js +12 -3
- package/dist/lib/resources/aws/messaging/eventBridgeRule.js +5 -2
- package/dist/lib/resources/aws/messaging/sns.js +7 -0
- package/dist/lib/resources/aws/messaging/sqs.js +2 -0
- package/dist/lib/resources/aws/networking/vpc.js +13 -2
- package/dist/lib/resources/aws/secrets/parameter.js +9 -7
- package/dist/lib/resources/aws/secrets/secret.js +5 -1
- package/dist/lib/resources/aws/storage/s3.d.ts +13 -0
- package/dist/lib/resources/aws/storage/s3.js +34 -7
- package/dist/lib/resources/aws/utilities/codeBuild.js +12 -1
- package/dist/lib/utils/costAllocationTags.d.ts +2 -2
- package/dist/lib/utils/costAllocationTags.js +2 -2
- package/package.json +3 -3
package/dist/lib/app.js
CHANGED
|
@@ -24,7 +24,7 @@ import { FjallLogger } from "./utils/validationLogger.js";
|
|
|
24
24
|
import { getManifestCollector, writeManifest } from "./utils/manifestWriter.js";
|
|
25
25
|
import { resetDnsRecordRegistry } from "./utils/dnsRecordRegistry.js";
|
|
26
26
|
import { toPascalCase, toKebab } from "./utils/capitaliseString.js";
|
|
27
|
-
import { COST_ALLOCATION_TAGS } from "./utils/costAllocationTags.js";
|
|
27
|
+
import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_OWNER } from "./utils/costAllocationTags.js";
|
|
28
28
|
import { DEFAULT_ORG_ID, resolveOrgId } from "./utils/cdkContext.js";
|
|
29
29
|
/**
|
|
30
30
|
* The basic corner-stone of all Fjall-hosted applications.
|
|
@@ -792,6 +792,13 @@ export class App extends CdkApp {
|
|
|
792
792
|
this.globalTags[COST_ALLOCATION_TAGS.OWNER] = orgId;
|
|
793
793
|
}
|
|
794
794
|
}
|
|
795
|
+
// Last-resort owner: without it, apps built directly on the construct
|
|
796
|
+
// library (no generator addTags block, no orgId context) ship every
|
|
797
|
+
// resource owner-less → MISSING_REQUIRED_TAGS across the estate.
|
|
798
|
+
if (this.globalTags[COST_ALLOCATION_TAGS.OWNER] === undefined) {
|
|
799
|
+
this.globalTags[COST_ALLOCATION_TAGS.OWNER] =
|
|
800
|
+
DEFAULT_COST_ALLOCATION_OWNER;
|
|
801
|
+
}
|
|
795
802
|
// Apply standard tags using Tags.of(this).add()
|
|
796
803
|
for (const [key, value] of Object.entries(this.globalTags)) {
|
|
797
804
|
Tags.of(this).add(key, value);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CfnOutput } from "aws-cdk-lib";
|
|
1
|
+
import { CfnOutput, Stack } from "aws-cdk-lib";
|
|
2
2
|
import { PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
3
3
|
import { EmailSubscription, UrlSubscription } from "aws-cdk-lib/aws-sns-subscriptions";
|
|
4
4
|
import { Construct } from "constructs";
|
|
@@ -38,6 +38,25 @@ export class SharedAlarmTopic extends Construct {
|
|
|
38
38
|
...(masterKey !== undefined && { masterKey })
|
|
39
39
|
});
|
|
40
40
|
this.topic = wrapped.getTopic();
|
|
41
|
+
// The wrapper's TLS-only TopicPolicy REPLACES the topic's implicit
|
|
42
|
+
// default policy, whose Allow was the SOLE authorisation for CloudWatch
|
|
43
|
+
// alarm-transition publishes — without this explicit Allow every alarm
|
|
44
|
+
// fires into a deny-only policy and pages nobody. Unlike the KMS path
|
|
45
|
+
// above, the topic-policy publish DOES carry the confused-deputy keys
|
|
46
|
+
// (the AWS-documented alarms-to-SNS shape), so scope to this account's
|
|
47
|
+
// alarms.
|
|
48
|
+
const stack = Stack.of(this);
|
|
49
|
+
this.topic.addToResourcePolicy(new PolicyStatement({
|
|
50
|
+
principals: [new ServicePrincipal("cloudwatch.amazonaws.com")],
|
|
51
|
+
actions: ["sns:Publish"],
|
|
52
|
+
resources: [wrapped.getTopicArn()],
|
|
53
|
+
conditions: {
|
|
54
|
+
StringEquals: { "aws:SourceAccount": stack.account },
|
|
55
|
+
ArnLike: {
|
|
56
|
+
"aws:SourceArn": `arn:${stack.partition}:cloudwatch:${stack.region}:${stack.account}:alarm:*`
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
41
60
|
for (const email of props?.subscriptions?.emails ?? []) {
|
|
42
61
|
this.topic.addSubscription(new EmailSubscription(email));
|
|
43
62
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Duration, RemovalPolicy } from "aws-cdk-lib";
|
|
2
2
|
import { CfnConfigurationRecorder, CfnDeliveryChannel } from "aws-cdk-lib/aws-config";
|
|
3
3
|
import { Role, ServicePrincipal, ManagedPolicy } from "aws-cdk-lib/aws-iam";
|
|
4
|
-
import {
|
|
4
|
+
import { BucketEncryption, BlockPublicAccess } from "aws-cdk-lib/aws-s3";
|
|
5
5
|
import { Construct } from "constructs";
|
|
6
|
+
import { NONCURRENT_VERSION_EXPIRY_DAYS, S3Bucket } from "../../resources/aws/storage/index.js";
|
|
6
7
|
/**
|
|
7
8
|
* AWS Config recorder with S3 delivery channel.
|
|
8
9
|
* Records configuration changes to all supported resources.
|
|
@@ -14,13 +15,23 @@ export class ConfigRecorder extends Construct {
|
|
|
14
15
|
super(scope, id);
|
|
15
16
|
const allResources = props?.allResources !== false;
|
|
16
17
|
const includeGlobalResources = props?.includeGlobalResources !== false;
|
|
17
|
-
//
|
|
18
|
-
|
|
18
|
+
// S3Bucket keeps the raw Bucket's construct id, so the deployed bucket's
|
|
19
|
+
// logical ID is unchanged (a rename would replace the bucket on redeploy);
|
|
20
|
+
// the wrapper supplies enforceSSL + multipart hygiene. Custom
|
|
21
|
+
// lifecycleRules bypass its automatic noncurrentVersionExpiration
|
|
22
|
+
// pairing — carried explicitly, or noncurrent versions accumulate forever.
|
|
23
|
+
const deliveryBucket = new S3Bucket(this, "DeliveryBucket", {
|
|
19
24
|
encryption: BucketEncryption.S3_MANAGED,
|
|
20
25
|
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
21
|
-
|
|
26
|
+
versioned: true,
|
|
22
27
|
removalPolicy: RemovalPolicy.RETAIN,
|
|
23
|
-
lifecycleRules: [
|
|
28
|
+
lifecycleRules: [
|
|
29
|
+
{
|
|
30
|
+
expiration: Duration.days(90),
|
|
31
|
+
noncurrentVersionExpiration: Duration.days(NONCURRENT_VERSION_EXPIRY_DAYS),
|
|
32
|
+
enabled: true
|
|
33
|
+
}
|
|
34
|
+
]
|
|
24
35
|
});
|
|
25
36
|
this.deliveryBucketName = deliveryBucket.bucketName;
|
|
26
37
|
// IAM role for Config service
|
|
@@ -201,7 +201,10 @@ export interface CodeLambdaProps extends BaseLambdaProps {
|
|
|
201
201
|
code: Code;
|
|
202
202
|
/** Handler function. Default: "index.handler" */
|
|
203
203
|
handler?: string;
|
|
204
|
-
/**
|
|
204
|
+
/**
|
|
205
|
+
* Lambda runtime. Default: NODEJS_24_X. A runtime AWS Lambda has
|
|
206
|
+
* deprecated fails synth.
|
|
207
|
+
*/
|
|
205
208
|
runtime?: Runtime;
|
|
206
209
|
}
|
|
207
210
|
/**
|
|
@@ -3,7 +3,7 @@ import { Connections } from "aws-cdk-lib/aws-ec2";
|
|
|
3
3
|
import { Repository } from "aws-cdk-lib/aws-ecr";
|
|
4
4
|
import { Construct } from "constructs";
|
|
5
5
|
import { processConnections } from "../../utils/connections.js";
|
|
6
|
-
import { LambdaFunction } from "../../resources/aws/compute/lambda.js";
|
|
6
|
+
import { LambdaFunction, validateRuntimeNotDeprecated } from "../../resources/aws/compute/lambda.js";
|
|
7
7
|
import { getOrCreateImageTagParameter } from "../../resources/aws/compute/imageTagParameter.js";
|
|
8
8
|
import { COMPUTE_DEFAULTS } from "./compute.js";
|
|
9
9
|
import { LAMBDA_ARCHITECTURE_VALUES } from "@fjall/util/manifest/schemas";
|
|
@@ -114,6 +114,9 @@ export function resolveLambdaDeployment(scope, id, props) {
|
|
|
114
114
|
runtime: Runtime.FROM_IMAGE
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
|
+
if (props.runtime !== undefined) {
|
|
118
|
+
validateRuntimeNotDeprecated(props.runtime, id);
|
|
119
|
+
}
|
|
117
120
|
return {
|
|
118
121
|
code: props.code,
|
|
119
122
|
handler: props.handler || COMPUTE_DEFAULTS.LAMBDA.HANDLER,
|
|
@@ -117,7 +117,9 @@ export interface AuroraDatabaseProps extends BaseDatabaseProps {
|
|
|
117
117
|
/**
|
|
118
118
|
* IP address to allow access from when publiclyAccessible is true.
|
|
119
119
|
* If not specified, access is restricted to VPC only even when public.
|
|
120
|
-
* Format: CIDR notation (e.g., "203.0.113.0/32" for single IP)
|
|
120
|
+
* Format: IPv4 CIDR notation (e.g., "203.0.113.0/32" for single IP).
|
|
121
|
+
* The world-open CIDR "0.0.0.0/0" raises a synth-time warning; a non-IPv4
|
|
122
|
+
* CIDR (e.g. "::/0") fails synth outright — the ingress peer is IPv4-only.
|
|
121
123
|
*/
|
|
122
124
|
allowedIpCidr?: string;
|
|
123
125
|
}
|
|
@@ -162,10 +164,11 @@ export interface InstanceDatabaseProps extends BaseDatabaseProps {
|
|
|
162
164
|
encryption?: EncryptionConfig;
|
|
163
165
|
publiclyAccessible?: boolean;
|
|
164
166
|
/**
|
|
165
|
-
*
|
|
166
|
-
* databases only.
|
|
167
|
+
* RDS IAM database authentication (default ON; pass false to opt out).
|
|
168
|
+
* Instance databases only. IAM principals granted via
|
|
167
169
|
* {@link RelationalDatabase.grantIamConnect} connect with short-lived
|
|
168
|
-
* `rds-db:connect` tokens instead of a stored password
|
|
170
|
+
* `rds-db:connect` tokens instead of a stored password; password auth keeps
|
|
171
|
+
* working in parallel. See ADR
|
|
169
172
|
* decisions/2026-06-17-rls-role-auth-and-launch-gating.md.
|
|
170
173
|
*/
|
|
171
174
|
iamAuthentication?: boolean;
|
|
@@ -189,6 +192,7 @@ export interface DynamoDBDatabaseProps {
|
|
|
189
192
|
stream?: "NEW_IMAGE" | "OLD_IMAGE" | "NEW_AND_OLD_IMAGES" | "KEYS_ONLY";
|
|
190
193
|
pointInTimeRecovery?: boolean;
|
|
191
194
|
encryption?: "AWS_OWNED" | "AWS_MANAGED" | "CUSTOMER_MANAGED";
|
|
195
|
+
deletionProtection?: boolean;
|
|
192
196
|
removalPolicy?: "DESTROY" | "RETAIN" | "SNAPSHOT";
|
|
193
197
|
}
|
|
194
198
|
/**
|
|
@@ -323,8 +327,9 @@ export declare class RelationalDatabase extends Construct implements IRelational
|
|
|
323
327
|
/**
|
|
324
328
|
* Grant an IAM principal permission to connect as `dbUsername` via RDS IAM
|
|
325
329
|
* database authentication. Instance databases only — Aurora uses a different
|
|
326
|
-
* mechanism, so this throws for non-Instance databases.
|
|
327
|
-
*
|
|
330
|
+
* mechanism, so this throws for non-Instance databases. IAM auth is on by
|
|
331
|
+
* default; this throws if the instance opted out with
|
|
332
|
+
* `iamAuthentication: false`. The L2 `grantConnect` scopes
|
|
328
333
|
* `rds-db:connect` to the exact `dbuser:<resourceId>/<dbUsername>` ARN — never
|
|
329
334
|
* a bare wildcard. See ADR
|
|
330
335
|
* decisions/2026-06-17-rls-role-auth-and-launch-gating.md.
|
|
@@ -12,7 +12,7 @@ import { Port } from "aws-cdk-lib/aws-ec2";
|
|
|
12
12
|
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
13
13
|
import { MIGRATION_SNAPSHOT_NAME_PREFIX } from "./interfaces/database.js";
|
|
14
14
|
import { pickLatestPrismaMigration } from "../../utils/migrationVersionResolvers.js";
|
|
15
|
-
import { SCHEMA_GATE_DB_PASSWORD_ENV, SCHEMA_GATE_DB_URL_BASE_ENV, SCHEMA_GATE_DB_USER_ENV } from "@fjall/util/migration";
|
|
15
|
+
import { MIGRATION_SNAPSHOT_RETENTION_DAYS_ENV, SCHEMA_GATE_DB_PASSWORD_ENV, SCHEMA_GATE_DB_URL_BASE_ENV, SCHEMA_GATE_DB_USER_ENV } from "@fjall/util/migration";
|
|
16
16
|
import { DatabaseClusterEngine, DatabaseInstanceEngine, AuroraPostgresEngineVersion, AuroraMysqlEngineVersion, PostgresEngineVersion, MysqlEngineVersion } from "aws-cdk-lib/aws-rds";
|
|
17
17
|
import { Duration } from "aws-cdk-lib";
|
|
18
18
|
export { isAwsManagedKey, isCMKRequested, AWS_MANAGED, USE_CMK } from "../../utils/databaseTypes.js";
|
|
@@ -222,6 +222,7 @@ export class DynamoDBDatabase extends Construct {
|
|
|
222
222
|
stream: props.stream,
|
|
223
223
|
pointInTimeRecovery: props.pointInTimeRecovery,
|
|
224
224
|
encryption: props.encryption,
|
|
225
|
+
deletionProtection: props.deletionProtection,
|
|
225
226
|
removalPolicy: props.removalPolicy
|
|
226
227
|
};
|
|
227
228
|
this.table = new DynamoDBTable(this, id, tableProps);
|
|
@@ -296,11 +297,13 @@ const SNAPSHOT_ACTION_TABLE = {
|
|
|
296
297
|
instance: {
|
|
297
298
|
createAction: "rds:CreateDBSnapshot",
|
|
298
299
|
describeAction: "rds:DescribeDBSnapshots",
|
|
300
|
+
deleteAction: "rds:DeleteDBSnapshot",
|
|
299
301
|
resourceArnSegment: "snapshot"
|
|
300
302
|
},
|
|
301
303
|
cluster: {
|
|
302
304
|
createAction: "rds:CreateDBClusterSnapshot",
|
|
303
305
|
describeAction: "rds:DescribeDBClusterSnapshots",
|
|
306
|
+
deleteAction: "rds:DeleteDBClusterSnapshot",
|
|
304
307
|
resourceArnSegment: "cluster-snapshot"
|
|
305
308
|
}
|
|
306
309
|
};
|
|
@@ -540,6 +543,10 @@ export class RelationalDatabase extends Construct {
|
|
|
540
543
|
actions: [
|
|
541
544
|
triple.createAction,
|
|
542
545
|
triple.describeAction,
|
|
546
|
+
// Delete is confined to fjall-premigrate-* by IAM: a Delete request's
|
|
547
|
+
// resource is always a snapshot ARN, which only the prefix glob below
|
|
548
|
+
// can match — never the instance/cluster ARN.
|
|
549
|
+
triple.deleteAction,
|
|
543
550
|
"rds:AddTagsToResource"
|
|
544
551
|
],
|
|
545
552
|
resources: [
|
|
@@ -605,6 +612,7 @@ export class RelationalDatabase extends Construct {
|
|
|
605
612
|
const target = this.getSnapshotTarget();
|
|
606
613
|
environment.SNAPSHOT_TARGET_KIND = target.kind;
|
|
607
614
|
environment.SNAPSHOT_TARGET_ARN = target.arn;
|
|
615
|
+
environment[MIGRATION_SNAPSHOT_RETENTION_DAYS_ENV] = String(this.database.getBackupRetentionDays());
|
|
608
616
|
}
|
|
609
617
|
const credentials = this.getCredentials();
|
|
610
618
|
const secretsImport = {
|
|
@@ -640,8 +648,9 @@ export class RelationalDatabase extends Construct {
|
|
|
640
648
|
/**
|
|
641
649
|
* Grant an IAM principal permission to connect as `dbUsername` via RDS IAM
|
|
642
650
|
* database authentication. Instance databases only — Aurora uses a different
|
|
643
|
-
* mechanism, so this throws for non-Instance databases.
|
|
644
|
-
*
|
|
651
|
+
* mechanism, so this throws for non-Instance databases. IAM auth is on by
|
|
652
|
+
* default; this throws if the instance opted out with
|
|
653
|
+
* `iamAuthentication: false`. The L2 `grantConnect` scopes
|
|
645
654
|
* `rds-db:connect` to the exact `dbuser:<resourceId>/<dbUsername>` ARN — never
|
|
646
655
|
* a bare wildcard. See ADR
|
|
647
656
|
* decisions/2026-06-17-rls-role-auth-and-launch-gating.md.
|
|
@@ -127,9 +127,12 @@ export interface IRelationalDatabaseBase extends IDatabase, IConnectable, IMigra
|
|
|
127
127
|
getSnapshotTarget(): SnapshotTarget;
|
|
128
128
|
/**
|
|
129
129
|
* Returns the IAM PolicyStatement granting the migration runner the rights
|
|
130
|
-
* needed to take and
|
|
131
|
-
*
|
|
132
|
-
* Snapshot name glob is bound to
|
|
130
|
+
* needed to take, tag, describe, and prune pre-migration snapshots of this
|
|
131
|
+
* database. Actions and resource ARN segment derive from
|
|
132
|
+
* `this.getSnapshotTarget().kind`. Snapshot name glob is bound to
|
|
133
|
+
* `MIGRATION_SNAPSHOT_NAME_PREFIX`; the delete action can only ever match
|
|
134
|
+
* that glob (a Delete request's resource is a snapshot ARN, never the
|
|
135
|
+
* database ARN), confining pruning to premigrate snapshots.
|
|
133
136
|
*/
|
|
134
137
|
getMigrationSnapshotPolicy(): PolicyStatement;
|
|
135
138
|
/**
|
|
@@ -36,6 +36,12 @@ export interface S3Props {
|
|
|
36
36
|
readonly versioned?: boolean;
|
|
37
37
|
readonly encryption?: "AES256" | "KMS";
|
|
38
38
|
readonly kmsKeyArn?: string;
|
|
39
|
+
/**
|
|
40
|
+
* S3 Bucket Keys for KMS-encrypted buckets — defaults to true when
|
|
41
|
+
* encryption resolves to KMS (cuts per-object KMS request charges); pass
|
|
42
|
+
* false to opt out. Ignored for non-KMS encryption.
|
|
43
|
+
*/
|
|
44
|
+
readonly bucketKeyEnabled?: boolean;
|
|
39
45
|
readonly backupVaultTier?: BackupTier;
|
|
40
46
|
readonly cors?: CorsRule[];
|
|
41
47
|
readonly deployment?: S3DeploymentConfig;
|
|
@@ -80,6 +80,9 @@ export class Storage extends Construct {
|
|
|
80
80
|
versioned: props.versioned,
|
|
81
81
|
encryption: toBucketEncryption(props.encryption),
|
|
82
82
|
encryptionKey,
|
|
83
|
+
...(props.bucketKeyEnabled !== undefined && {
|
|
84
|
+
bucketKeyEnabled: props.bucketKeyEnabled
|
|
85
|
+
}),
|
|
83
86
|
backupVaultTier: props.backupVaultTier,
|
|
84
87
|
publicReadAccess: props.publicReadAccess,
|
|
85
88
|
websiteHosting: props.websiteHosting,
|
|
@@ -16,6 +16,9 @@ export function createApplicationLoadBalancer(scope, id, options) {
|
|
|
16
16
|
// ALB-fronted containers receive it as FJALL_ALB_IDLE_TIMEOUT_SECONDS
|
|
17
17
|
// (ecsTaskDefinition.ts) for keep-alive tuning — one constant, both sides.
|
|
18
18
|
idleTimeout: Duration.seconds(DEFAULT_ALB_IDLE_TIMEOUT_SECONDS),
|
|
19
|
+
// ELB.4 — invalid HTTP headers are a request-smuggling surface; drop them
|
|
20
|
+
// at the ALB. Attribute-only, so existing LBs update in place.
|
|
21
|
+
dropInvalidHeaderFields: true,
|
|
19
22
|
...(options.securityGroup && { securityGroup: options.securityGroup }),
|
|
20
23
|
...(options.loadBalancerName && {
|
|
21
24
|
loadBalancerName: options.loadBalancerName
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Cluster as CdkCluster, ContainerInsights } from "aws-cdk-lib/aws-ecs";
|
|
1
|
+
import { Cluster as CdkCluster, ContainerInsights, ExecuteCommandLogging } from "aws-cdk-lib/aws-ecs";
|
|
2
2
|
import { Connections, Port } from "aws-cdk-lib/aws-ec2";
|
|
3
3
|
import { Construct } from "constructs";
|
|
4
4
|
import { CfnOutput, Aspects } from "aws-cdk-lib";
|
|
5
5
|
import { processConnections } from "../../../utils/connections.js";
|
|
6
|
+
import { DEFAULT_FRAMEWORK_LOG_RETENTION, LogGroup } from "../logging/logGroup.js";
|
|
6
7
|
import { stackScopedExportName } from "../../../utils/exportNaming.js";
|
|
7
8
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
8
9
|
import { createEcsServiceAlarms, createEcsTaskStopWatchdog, createLogPatternAlarms } from "../monitoring/index.js";
|
|
@@ -382,13 +383,21 @@ export default class EcsCluster extends Construct {
|
|
|
382
383
|
}
|
|
383
384
|
addCluster(props) {
|
|
384
385
|
const needsFargate = this.anyServiceUsesFargate();
|
|
386
|
+
// Every service enables execute command; without an OVERRIDE here the
|
|
387
|
+
// exec session output interleaves into the container app log groups
|
|
388
|
+
// (DEFAULT mode). No kmsKey — session encryption is tier-gated.
|
|
389
|
+
const execAuditLogGroup = new LogGroup(this, `${props.clusterName}ExecAuditLogGroup`, { retention: DEFAULT_FRAMEWORK_LOG_RETENTION });
|
|
385
390
|
const cluster = new CdkCluster(this, `${props.clusterName}Cluster`, {
|
|
386
391
|
vpc: props.vpc,
|
|
387
392
|
clusterName: props.clusterName,
|
|
388
393
|
containerInsightsV2: props.containerInsights === true
|
|
389
394
|
? ContainerInsights.ENABLED
|
|
390
395
|
: ContainerInsights.DISABLED,
|
|
391
|
-
enableFargateCapacityProviders: needsFargate
|
|
396
|
+
enableFargateCapacityProviders: needsFargate,
|
|
397
|
+
executeCommandConfiguration: {
|
|
398
|
+
logging: ExecuteCommandLogging.OVERRIDE,
|
|
399
|
+
logConfiguration: { cloudWatchLogGroup: execAuditLogGroup }
|
|
400
|
+
}
|
|
392
401
|
});
|
|
393
402
|
new CfnOutput(this, `${this.outputName}DeployableCluster`, {
|
|
394
403
|
key: `${this.outputName}DeployableCluster`,
|
|
@@ -10,6 +10,23 @@ import { type KeyValue } from "../../../types.js";
|
|
|
10
10
|
import { type SecretImport } from "../secrets/index.js";
|
|
11
11
|
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
12
12
|
import { type LambdaAlarmThresholds } from "../monitoring/index.js";
|
|
13
|
+
/**
|
|
14
|
+
* Runtime names AWS Lambda has deprecated, mirrored from the `@deprecated`
|
|
15
|
+
* markers on `Runtime`'s static members in the vendored aws-cdk-lib
|
|
16
|
+
* (aws-lambda/lib/runtime.d.ts). The SDK exposes no runtime-readable
|
|
17
|
+
* deprecation flag on `Runtime` instances, so this set can drift when
|
|
18
|
+
* aws-cdk-lib is upgraded — lambda-deprecated-runtime.test.ts re-derives it
|
|
19
|
+
* from the vendored declaration file and fails on any mismatch.
|
|
20
|
+
*/
|
|
21
|
+
export declare const DEPRECATED_RUNTIME_NAMES: ReadonlySet<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Throw at synth time when a deprecated Lambda runtime is configured —
|
|
24
|
+
* shift-left of the posture scan's LAMBDA_DEPRECATED_RUNTIME finding, which
|
|
25
|
+
* previously let the deploy succeed and flagged it days later. A deprecated
|
|
26
|
+
* runtime receives no security patches and AWS eventually blocks function
|
|
27
|
+
* creation on it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateRuntimeNotDeprecated(runtime: Runtime, functionId: string): void;
|
|
13
30
|
export interface LambdaFunctionProps {
|
|
14
31
|
code: Code;
|
|
15
32
|
handler: string;
|
|
@@ -53,6 +70,16 @@ export interface LambdaFunctionProps {
|
|
|
53
70
|
functionUrlCors?: FunctionUrlCorsOptions;
|
|
54
71
|
/** Invoke mode for Function URL. Use RESPONSE_STREAM for Lambda streaming. */
|
|
55
72
|
functionUrlInvokeMode?: InvokeMode;
|
|
73
|
+
/**
|
|
74
|
+
* Failure destination for asynchronous invocations. S3 event notifications
|
|
75
|
+
* invoke the function asynchronously, and a failed event is silently
|
|
76
|
+
* dropped after the default two retries. Unless set to `false`, the first
|
|
77
|
+
* `addS3EventSource` call lazily creates one SQS queue for the function
|
|
78
|
+
* (SSL-enforced, 14-day retention) and wires it as the async dead-letter
|
|
79
|
+
* queue; pass an existing queue to reuse it instead. Functions with no
|
|
80
|
+
* async event source get no queue.
|
|
81
|
+
*/
|
|
82
|
+
asyncFailureQueue?: IQueue | false;
|
|
56
83
|
environment?: KeyValue;
|
|
57
84
|
secrets?: string[];
|
|
58
85
|
ssmSecretsPath?: string;
|
|
@@ -79,6 +106,8 @@ export declare class SingletonFunction extends singletonFunction {
|
|
|
79
106
|
}
|
|
80
107
|
export declare class LambdaFunction extends Function {
|
|
81
108
|
private functionUrlValue?;
|
|
109
|
+
private readonly asyncFailureQueueProp?;
|
|
110
|
+
private asyncFailureQueueWired?;
|
|
82
111
|
constructor(scope: Construct, id: string, props: LambdaFunctionProps);
|
|
83
112
|
/**
|
|
84
113
|
* The Lambda's execution role (auto-generated by CDK)
|
|
@@ -114,6 +143,9 @@ export declare class LambdaFunction extends Function {
|
|
|
114
143
|
* Add an S3 bucket as an event source for this Lambda function.
|
|
115
144
|
* This will trigger the Lambda when objects are created, modified, or deleted.
|
|
116
145
|
* Useful for ISR cache invalidation and file processing workflows.
|
|
146
|
+
*
|
|
147
|
+
* S3 invokes asynchronously, so the first call also wires the async
|
|
148
|
+
* failure queue (see `asyncFailureQueue`) unless it is set to `false`.
|
|
117
149
|
*/
|
|
118
150
|
addS3EventSource(bucket: Bucket, options?: {
|
|
119
151
|
events?: Array<"OBJECT_CREATED" | "OBJECT_REMOVED" | "OBJECT_CREATED_PUT" | "OBJECT_CREATED_POST" | "OBJECT_CREATED_COPY" | "OBJECT_REMOVED_DELETE">;
|
|
@@ -122,6 +154,20 @@ export declare class LambdaFunction extends Function {
|
|
|
122
154
|
suffix?: string;
|
|
123
155
|
}>;
|
|
124
156
|
}): void;
|
|
157
|
+
/**
|
|
158
|
+
* Get the SQS queue wired as the async-invocation failure destination, if
|
|
159
|
+
* one has been created or supplied.
|
|
160
|
+
*/
|
|
161
|
+
getAsyncFailureQueue(): IQueue | undefined;
|
|
162
|
+
/**
|
|
163
|
+
* Wire the async-invocation dead-letter queue on first use. Without one,
|
|
164
|
+
* an event that fails all retries is silently dropped. `Function`'s
|
|
165
|
+
* `deadLetterQueue` prop is constructor-only and the queue is only wanted
|
|
166
|
+
* once an async event source exists, so reach the L1 `CfnFunction` and
|
|
167
|
+
* mirror what the prop does: DeadLetterConfig plus an sqs:SendMessage
|
|
168
|
+
* grant to the execution role.
|
|
169
|
+
*/
|
|
170
|
+
private ensureAsyncFailureQueue;
|
|
125
171
|
/**
|
|
126
172
|
* Add secrets support using AWS Parameters and Secrets Lambda Extension.
|
|
127
173
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CfnOutput, Duration, Stack, Size } from "aws-cdk-lib";
|
|
1
|
+
import { Annotations, CfnOutput, Duration, Stack, Size } from "aws-cdk-lib";
|
|
2
2
|
import { SingletonFunction as singletonFunction, Function, Code, Architecture, FunctionUrlAuthType, StartingPosition, LayerVersion } from "aws-cdk-lib/aws-lambda";
|
|
3
3
|
import { FactName } from "aws-cdk-lib/region-info";
|
|
4
4
|
import path from "node:path";
|
|
@@ -8,6 +8,8 @@ import { SqsEventSource, DynamoEventSource, S3EventSource } from "aws-cdk-lib/aw
|
|
|
8
8
|
import { EventType } from "aws-cdk-lib/aws-s3";
|
|
9
9
|
import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
|
|
10
10
|
import { DEFAULT_FRAMEWORK_LOG_RETENTION, LogGroup } from "../logging/logGroup.js";
|
|
11
|
+
import { Queue, QueueEncryption } from "aws-cdk-lib/aws-sqs";
|
|
12
|
+
import { SQS_LIMITS } from "../messaging/sqs.js";
|
|
11
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
14
|
import { buildParameterPath } from "@fjall/util";
|
|
13
15
|
import { resolveImportedSecret } from "../secrets/index.js";
|
|
@@ -66,6 +68,54 @@ const SECRETS_EXTENSION = {
|
|
|
66
68
|
* mis-tune alarms relative to runtime behaviour.
|
|
67
69
|
*/
|
|
68
70
|
const LAMBDA_DEFAULT_TIMEOUT_SECONDS = 300;
|
|
71
|
+
/**
|
|
72
|
+
* Runtime names AWS Lambda has deprecated, mirrored from the `@deprecated`
|
|
73
|
+
* markers on `Runtime`'s static members in the vendored aws-cdk-lib
|
|
74
|
+
* (aws-lambda/lib/runtime.d.ts). The SDK exposes no runtime-readable
|
|
75
|
+
* deprecation flag on `Runtime` instances, so this set can drift when
|
|
76
|
+
* aws-cdk-lib is upgraded — lambda-deprecated-runtime.test.ts re-derives it
|
|
77
|
+
* from the vendored declaration file and fails on any mismatch.
|
|
78
|
+
*/
|
|
79
|
+
export const DEPRECATED_RUNTIME_NAMES = new Set([
|
|
80
|
+
"nodejs",
|
|
81
|
+
"nodejs4.3",
|
|
82
|
+
"nodejs6.10",
|
|
83
|
+
"nodejs8.10",
|
|
84
|
+
"nodejs10.x",
|
|
85
|
+
"nodejs12.x",
|
|
86
|
+
"nodejs14.x",
|
|
87
|
+
"nodejs16.x",
|
|
88
|
+
"nodejs18.x",
|
|
89
|
+
"python2.7",
|
|
90
|
+
"python3.6",
|
|
91
|
+
"python3.7",
|
|
92
|
+
"python3.8",
|
|
93
|
+
"python3.9",
|
|
94
|
+
"java8",
|
|
95
|
+
"dotnet6",
|
|
96
|
+
"dotnetcore1.0",
|
|
97
|
+
"dotnetcore2.0",
|
|
98
|
+
"dotnetcore2.1",
|
|
99
|
+
"dotnetcore3.1",
|
|
100
|
+
"go1.x",
|
|
101
|
+
"ruby2.5",
|
|
102
|
+
"ruby2.7",
|
|
103
|
+
"provided"
|
|
104
|
+
]);
|
|
105
|
+
/**
|
|
106
|
+
* Throw at synth time when a deprecated Lambda runtime is configured —
|
|
107
|
+
* shift-left of the posture scan's LAMBDA_DEPRECATED_RUNTIME finding, which
|
|
108
|
+
* previously let the deploy succeed and flagged it days later. A deprecated
|
|
109
|
+
* runtime receives no security patches and AWS eventually blocks function
|
|
110
|
+
* creation on it.
|
|
111
|
+
*/
|
|
112
|
+
export function validateRuntimeNotDeprecated(runtime, functionId) {
|
|
113
|
+
if (DEPRECATED_RUNTIME_NAMES.has(runtime.name)) {
|
|
114
|
+
throw new Error(`Lambda '${functionId}': runtime '${runtime.name}' is deprecated by ` +
|
|
115
|
+
`AWS Lambda. Migrate to a supported runtime — e.g. ` +
|
|
116
|
+
`Runtime.NODEJS_24_X for Node.js.`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
69
119
|
/**
|
|
70
120
|
* Stable, deterministic uuid for a SingletonFunction so its logical ID
|
|
71
121
|
* (`SingletonLambda${uuid-without-dashes}`) does not drift across synths. A
|
|
@@ -86,6 +136,7 @@ function deriveStableSingletonUuid(scope, id) {
|
|
|
86
136
|
}
|
|
87
137
|
export class SingletonFunction extends singletonFunction {
|
|
88
138
|
constructor(scope, id, props) {
|
|
139
|
+
validateRuntimeNotDeprecated(props.runtime, id);
|
|
89
140
|
super(scope, id, {
|
|
90
141
|
...props,
|
|
91
142
|
uuid: props.uuid ?? deriveStableSingletonUuid(scope, id),
|
|
@@ -112,7 +163,10 @@ export class SingletonFunction extends singletonFunction {
|
|
|
112
163
|
}
|
|
113
164
|
export class LambdaFunction extends Function {
|
|
114
165
|
functionUrlValue;
|
|
166
|
+
asyncFailureQueueProp;
|
|
167
|
+
asyncFailureQueueWired;
|
|
115
168
|
constructor(scope, id, props) {
|
|
169
|
+
validateRuntimeNotDeprecated(props.runtime, id);
|
|
116
170
|
const vpcSubnets = props.vpc
|
|
117
171
|
? { subnetType: resolvePrivateSubnetType(props.vpc) }
|
|
118
172
|
: undefined;
|
|
@@ -130,6 +184,7 @@ export class LambdaFunction extends Function {
|
|
|
130
184
|
retention: props.logGroupRetention ?? DEFAULT_FRAMEWORK_LOG_RETENTION
|
|
131
185
|
})
|
|
132
186
|
});
|
|
187
|
+
this.asyncFailureQueueProp = props.asyncFailureQueue;
|
|
133
188
|
addPoliciesToRole(this, props.inlinePolicy);
|
|
134
189
|
applyRoleDescription(this, props.roleDescription);
|
|
135
190
|
applyRolePathAndName(this, props.rolePath, props.roleName);
|
|
@@ -137,11 +192,19 @@ export class LambdaFunction extends Function {
|
|
|
137
192
|
// Sanitise id for CloudFormation output keys (must be alphanumeric)
|
|
138
193
|
const outputName = toPascalCase(id);
|
|
139
194
|
if (props.enableFunctionUrl) {
|
|
195
|
+
const authType = props.functionUrlAuthType ?? FunctionUrlAuthType.AWS_IAM;
|
|
140
196
|
const functionUrl = this.addFunctionUrl({
|
|
141
|
-
authType
|
|
197
|
+
authType,
|
|
142
198
|
cors: props.functionUrlCors,
|
|
143
199
|
invokeMode: props.functionUrlInvokeMode
|
|
144
200
|
});
|
|
201
|
+
if (authType === FunctionUrlAuthType.NONE &&
|
|
202
|
+
props.reservedConcurrentExecutions === undefined) {
|
|
203
|
+
Annotations.of(this).addWarning(`Lambda '${id}' exposes a public Function URL (authType NONE) ` +
|
|
204
|
+
`without reservedConcurrentExecutions. An unauthenticated caller ` +
|
|
205
|
+
`can drive unbounded invocations, taking spend and account-wide ` +
|
|
206
|
+
`concurrency with it — set reservedConcurrentExecutions to cap it.`);
|
|
207
|
+
}
|
|
145
208
|
this.functionUrlValue = functionUrl.url;
|
|
146
209
|
new CfnOutput(this, `${outputName}FunctionUrl`, {
|
|
147
210
|
key: `${outputName}FunctionUrl`,
|
|
@@ -218,6 +281,9 @@ export class LambdaFunction extends Function {
|
|
|
218
281
|
* Add an S3 bucket as an event source for this Lambda function.
|
|
219
282
|
* This will trigger the Lambda when objects are created, modified, or deleted.
|
|
220
283
|
* Useful for ISR cache invalidation and file processing workflows.
|
|
284
|
+
*
|
|
285
|
+
* S3 invokes asynchronously, so the first call also wires the async
|
|
286
|
+
* failure queue (see `asyncFailureQueue`) unless it is set to `false`.
|
|
221
287
|
*/
|
|
222
288
|
addS3EventSource(bucket, options) {
|
|
223
289
|
const eventTypes = (options?.events ?? ["OBJECT_CREATED"]).map((event) => {
|
|
@@ -246,6 +312,38 @@ export class LambdaFunction extends Function {
|
|
|
246
312
|
};
|
|
247
313
|
const eventSource = new S3EventSource(bucket, s3EventSourceProps);
|
|
248
314
|
this.addEventSource(eventSource);
|
|
315
|
+
this.ensureAsyncFailureQueue();
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Get the SQS queue wired as the async-invocation failure destination, if
|
|
319
|
+
* one has been created or supplied.
|
|
320
|
+
*/
|
|
321
|
+
getAsyncFailureQueue() {
|
|
322
|
+
return this.asyncFailureQueueWired;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Wire the async-invocation dead-letter queue on first use. Without one,
|
|
326
|
+
* an event that fails all retries is silently dropped. `Function`'s
|
|
327
|
+
* `deadLetterQueue` prop is constructor-only and the queue is only wanted
|
|
328
|
+
* once an async event source exists, so reach the L1 `CfnFunction` and
|
|
329
|
+
* mirror what the prop does: DeadLetterConfig plus an sqs:SendMessage
|
|
330
|
+
* grant to the execution role.
|
|
331
|
+
*/
|
|
332
|
+
ensureAsyncFailureQueue() {
|
|
333
|
+
if (this.asyncFailureQueueProp === false)
|
|
334
|
+
return;
|
|
335
|
+
if (this.asyncFailureQueueWired !== undefined)
|
|
336
|
+
return;
|
|
337
|
+
const queue = this.asyncFailureQueueProp ??
|
|
338
|
+
new Queue(this, "AsyncFailureQueue", {
|
|
339
|
+
enforceSSL: true,
|
|
340
|
+
encryption: QueueEncryption.SQS_MANAGED,
|
|
341
|
+
retentionPeriod: Duration.days(SQS_LIMITS.DEAD_LETTER_QUEUE.DEFAULT_RETENTION_DAYS)
|
|
342
|
+
});
|
|
343
|
+
this.asyncFailureQueueWired = queue;
|
|
344
|
+
const cfnFunction = this.node.defaultChild;
|
|
345
|
+
cfnFunction.deadLetterConfig = { targetArn: queue.queueArn };
|
|
346
|
+
queue.grantSendMessages(this);
|
|
249
347
|
}
|
|
250
348
|
/**
|
|
251
349
|
* Add secrets support using AWS Parameters and Secrets Lambda Extension.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ListenerAction } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
1
|
+
import { ListenerAction, SslPolicy } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
2
2
|
/**
|
|
3
3
|
* Context-free ALB listener factory shared by the ECS cluster path and the dev
|
|
4
4
|
* substrate. When `default404` is set, the listener's default action is a plain
|
|
@@ -25,7 +25,11 @@ export function addRoutingListener(loadBalancer, id, options) {
|
|
|
25
25
|
certificates: [
|
|
26
26
|
options.certificate,
|
|
27
27
|
...(options.additionalCertificates ?? [])
|
|
28
|
-
]
|
|
28
|
+
],
|
|
29
|
+
// TLS 1.3/1.2 only (ELBSecurityPolicy-TLS13-1-2-2021-06) — drops
|
|
30
|
+
// TLS 1.0/1.1 clients. HTTPS-only: CloudFormation rejects SslPolicy on
|
|
31
|
+
// a plain HTTP listener.
|
|
32
|
+
sslPolicy: SslPolicy.RECOMMENDED_TLS
|
|
29
33
|
}),
|
|
30
34
|
...(defaultAction !== undefined && { defaultAction })
|
|
31
35
|
});
|
|
@@ -25,6 +25,7 @@ export interface DynamoDBTableProps {
|
|
|
25
25
|
stream?: "NEW_IMAGE" | "OLD_IMAGE" | "NEW_AND_OLD_IMAGES" | "KEYS_ONLY";
|
|
26
26
|
pointInTimeRecovery?: boolean;
|
|
27
27
|
encryption?: "AWS_OWNED" | "AWS_MANAGED" | "CUSTOMER_MANAGED";
|
|
28
|
+
deletionProtection?: boolean;
|
|
28
29
|
removalPolicy?: "DESTROY" | "RETAIN" | "SNAPSHOT";
|
|
29
30
|
}
|
|
30
31
|
export declare class DynamoDBTable extends Construct {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { CfnOutput } from "aws-cdk-lib";
|
|
3
3
|
import { toRemovalPolicy } from "../../../utils/removalPolicy.js";
|
|
4
|
-
import { Table, AttributeType, BillingMode, StreamViewType, ProjectionType } from "aws-cdk-lib/aws-dynamodb";
|
|
4
|
+
import { Table, AttributeType, BillingMode, StreamViewType, ProjectionType, TableEncryption } from "aws-cdk-lib/aws-dynamodb";
|
|
5
5
|
function toAttributeType(type) {
|
|
6
6
|
switch (type) {
|
|
7
7
|
case "S":
|
|
@@ -21,6 +21,17 @@ function toBillingMode(mode) {
|
|
|
21
21
|
return BillingMode.PAY_PER_REQUEST;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
function toTableEncryption(encryption) {
|
|
25
|
+
switch (encryption) {
|
|
26
|
+
case "AWS_OWNED":
|
|
27
|
+
return TableEncryption.DEFAULT;
|
|
28
|
+
case "CUSTOMER_MANAGED":
|
|
29
|
+
return TableEncryption.CUSTOMER_MANAGED;
|
|
30
|
+
case "AWS_MANAGED":
|
|
31
|
+
default:
|
|
32
|
+
return TableEncryption.AWS_MANAGED;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
24
35
|
function toStreamViewType(stream) {
|
|
25
36
|
switch (stream) {
|
|
26
37
|
case "NEW_IMAGE":
|
|
@@ -72,6 +83,8 @@ export class DynamoDBTable extends Construct {
|
|
|
72
83
|
pointInTimeRecoverySpecification: {
|
|
73
84
|
pointInTimeRecoveryEnabled: props.pointInTimeRecovery ?? true
|
|
74
85
|
},
|
|
86
|
+
encryption: toTableEncryption(props.encryption),
|
|
87
|
+
deletionProtection: props.deletionProtection ?? false,
|
|
75
88
|
removalPolicy: toRemovalPolicy(props.removalPolicy),
|
|
76
89
|
timeToLiveAttribute: props.timeToLiveAttribute
|
|
77
90
|
});
|
|
@@ -97,6 +97,7 @@ export declare class RdsAurora extends Construct implements IConnectable {
|
|
|
97
97
|
private databaseCredentials;
|
|
98
98
|
private cfnCluster?;
|
|
99
99
|
private databaseNameValue;
|
|
100
|
+
private backupRetentionDaysValue;
|
|
100
101
|
constructor(scope: Construct, id: string, props: RdsProps);
|
|
101
102
|
private buildReaders;
|
|
102
103
|
private addProxy;
|
|
@@ -108,6 +109,13 @@ export declare class RdsAurora extends Construct implements IConnectable {
|
|
|
108
109
|
getSnapshotTarget(): Extract<SnapshotTarget, {
|
|
109
110
|
kind: "cluster";
|
|
110
111
|
}>;
|
|
112
|
+
/**
|
|
113
|
+
* Effective automated-backup retention in whole days (the applied
|
|
114
|
+
* `backupRetention`, defaulted). Feeds `MIGRATION_SNAPSHOT_RETENTION_DAYS`
|
|
115
|
+
* so the migration runner prunes pre-migration snapshots against the same
|
|
116
|
+
* window the cluster's PITR actually covers.
|
|
117
|
+
*/
|
|
118
|
+
getBackupRetentionDays(): number;
|
|
111
119
|
getDatabaseName(): string;
|
|
112
120
|
getConnectionString(): string;
|
|
113
121
|
getCfnCluster(): CfnDBCluster | undefined;
|
|
@@ -62,6 +62,7 @@ export class RdsAurora extends Construct {
|
|
|
62
62
|
databaseCredentials;
|
|
63
63
|
cfnCluster;
|
|
64
64
|
databaseNameValue;
|
|
65
|
+
backupRetentionDaysValue;
|
|
65
66
|
constructor(scope, id, props) {
|
|
66
67
|
super(scope, id);
|
|
67
68
|
validateServerlessV2DevKnobs(props);
|
|
@@ -123,6 +124,11 @@ export class RdsAurora extends Construct {
|
|
|
123
124
|
}
|
|
124
125
|
// Allow external access when publicly accessible with allowed IP
|
|
125
126
|
if (props.publiclyAccessible && props.allowedIpCidr) {
|
|
127
|
+
// "::/0" is deliberately not matched here: Peer.ipv4 below throws on any
|
|
128
|
+
// non-IPv4 CIDR before a warning could ever render.
|
|
129
|
+
if (props.allowedIpCidr === "0.0.0.0/0") {
|
|
130
|
+
Annotations.of(this).addWarningV2("@fjall/components-infrastructure:rdsWorldOpenIngress", `Database '${this.databaseNameValue}' is publicly accessible and allowedIpCidr '${props.allowedIpCidr}' opens its database port to the entire internet. Scope allowedIpCidr to the addresses that need access (e.g. "203.0.113.4/32").`);
|
|
131
|
+
}
|
|
126
132
|
clusterSecurityGroup.addIngressRule(Peer.ipv4(props.allowedIpCidr), Port.tcp(this.port), "Allow external access from allowed IP");
|
|
127
133
|
}
|
|
128
134
|
this.connections = new Connections({
|
|
@@ -163,6 +169,8 @@ export class RdsAurora extends Construct {
|
|
|
163
169
|
const vpcSubnets = props.publiclyAccessible
|
|
164
170
|
? { subnetType: SubnetType.PUBLIC }
|
|
165
171
|
: { subnetType: SubnetType.PRIVATE_WITH_EGRESS };
|
|
172
|
+
this.backupRetentionDaysValue =
|
|
173
|
+
props.backupRetention ?? RDS_DEFAULTS.BACKUP_RETENTION_DEFAULT_DAYS;
|
|
166
174
|
// Common props shared across all cluster creation paths
|
|
167
175
|
const baseClusterProps = {
|
|
168
176
|
vpc: props.vpc,
|
|
@@ -171,7 +179,7 @@ export class RdsAurora extends Construct {
|
|
|
171
179
|
engine,
|
|
172
180
|
parameterGroup,
|
|
173
181
|
backup: {
|
|
174
|
-
retention: Duration.days(
|
|
182
|
+
retention: Duration.days(this.backupRetentionDaysValue)
|
|
175
183
|
},
|
|
176
184
|
storageEncrypted: true,
|
|
177
185
|
...(storageEncryptionKey && { storageEncryptionKey }),
|
|
@@ -361,6 +369,15 @@ export class RdsAurora extends Construct {
|
|
|
361
369
|
getSnapshotTarget() {
|
|
362
370
|
return { kind: "cluster", arn: this.databaseCluster.clusterArn };
|
|
363
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* Effective automated-backup retention in whole days (the applied
|
|
374
|
+
* `backupRetention`, defaulted). Feeds `MIGRATION_SNAPSHOT_RETENTION_DAYS`
|
|
375
|
+
* so the migration runner prunes pre-migration snapshots against the same
|
|
376
|
+
* window the cluster's PITR actually covers.
|
|
377
|
+
*/
|
|
378
|
+
getBackupRetentionDays() {
|
|
379
|
+
return this.backupRetentionDaysValue;
|
|
380
|
+
}
|
|
364
381
|
getDatabaseName() {
|
|
365
382
|
return this.databaseNameValue;
|
|
366
383
|
}
|
|
@@ -81,6 +81,13 @@ export declare class RdsAuroraGlobal extends Construct implements IConnectable {
|
|
|
81
81
|
kind: "cluster";
|
|
82
82
|
}>;
|
|
83
83
|
getDatabaseName(): string;
|
|
84
|
+
/**
|
|
85
|
+
* Effective automated-backup retention in whole days, delegated to the
|
|
86
|
+
* regional cluster that actually applies the default. Feeds
|
|
87
|
+
* `MIGRATION_SNAPSHOT_RETENTION_DAYS` so the migration runner prunes
|
|
88
|
+
* pre-migration snapshots against the same window PITR actually covers.
|
|
89
|
+
*/
|
|
90
|
+
getBackupRetentionDays(): number;
|
|
84
91
|
getConnectionString(): string;
|
|
85
92
|
}
|
|
86
93
|
export {};
|
|
@@ -182,6 +182,21 @@ export class RdsAuroraGlobal extends Construct {
|
|
|
182
182
|
return this.regionalCluster.getDatabaseName();
|
|
183
183
|
throw new Error("No Aurora cluster available to return database name from");
|
|
184
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Effective automated-backup retention in whole days, delegated to the
|
|
187
|
+
* regional cluster that actually applies the default. Feeds
|
|
188
|
+
* `MIGRATION_SNAPSHOT_RETENTION_DAYS` so the migration runner prunes
|
|
189
|
+
* pre-migration snapshots against the same window PITR actually covers.
|
|
190
|
+
*/
|
|
191
|
+
getBackupRetentionDays() {
|
|
192
|
+
if (this.primaryCluster) {
|
|
193
|
+
return this.primaryCluster.getBackupRetentionDays();
|
|
194
|
+
}
|
|
195
|
+
if (this.regionalCluster) {
|
|
196
|
+
return this.regionalCluster.getBackupRetentionDays();
|
|
197
|
+
}
|
|
198
|
+
throw new Error("No Aurora cluster available to return backup retention from");
|
|
199
|
+
}
|
|
185
200
|
getConnectionString() {
|
|
186
201
|
if (this.primaryCluster)
|
|
187
202
|
return this.primaryCluster.getConnectionString();
|
|
@@ -33,11 +33,11 @@ interface RdsProps {
|
|
|
33
33
|
publiclyAccessible?: boolean;
|
|
34
34
|
deletionProtection?: boolean;
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
36
|
+
* RDS IAM database authentication on the instance (and its read replica).
|
|
37
|
+
* Defaults to ON — free, and it grants nothing until {@link grantIamConnect};
|
|
38
|
+
* password auth keeps working in parallel. Pass false to opt out. For stacks
|
|
39
|
+
* deployed under the old opt-in default the false→true flip is a
|
|
40
|
+
* No-interruption CFN modify, so redeploying is safe. See ADR
|
|
41
41
|
* decisions/2026-06-17-rls-role-auth-and-launch-gating.md.
|
|
42
42
|
*/
|
|
43
43
|
iamAuthentication?: boolean;
|
|
@@ -71,6 +71,7 @@ export declare class RdsInstance extends Construct implements IConnectable {
|
|
|
71
71
|
private databaseProxySecurityGroup?;
|
|
72
72
|
private readReplicaSecurityGroup?;
|
|
73
73
|
private databaseNameValue;
|
|
74
|
+
private backupRetentionDaysValue;
|
|
74
75
|
private readonly constructId;
|
|
75
76
|
constructor(scope: Construct, id: string, props: RdsProps);
|
|
76
77
|
private addDatabase;
|
|
@@ -84,12 +85,19 @@ export declare class RdsInstance extends Construct implements IConnectable {
|
|
|
84
85
|
getSnapshotTarget(): Extract<SnapshotTarget, {
|
|
85
86
|
kind: "instance";
|
|
86
87
|
}>;
|
|
88
|
+
/**
|
|
89
|
+
* Effective automated-backup retention in whole days (the applied
|
|
90
|
+
* `backupRetention`, defaulted). Feeds `MIGRATION_SNAPSHOT_RETENTION_DAYS`
|
|
91
|
+
* so the migration runner prunes pre-migration snapshots against the same
|
|
92
|
+
* window the instance's PITR actually covers.
|
|
93
|
+
*/
|
|
94
|
+
getBackupRetentionDays(): number;
|
|
87
95
|
getDatabaseName(): string;
|
|
88
96
|
getConnectionString(): string;
|
|
89
97
|
/**
|
|
90
98
|
* Grant an IAM principal permission to connect as `dbUsername` via RDS IAM
|
|
91
|
-
* database authentication.
|
|
92
|
-
* `iamAuthentication:
|
|
99
|
+
* database authentication. Enabled by default; throws if the instance opted
|
|
100
|
+
* out with `iamAuthentication: false`. Delegates to the L2 `grantConnect`, which scopes
|
|
93
101
|
* `rds-db:connect` to the exact `dbuser:<dbiResourceId>/<dbUsername>` ARN —
|
|
94
102
|
* never a bare wildcard. See ADR
|
|
95
103
|
* decisions/2026-06-17-rls-role-auth-and-launch-gating.md.
|
|
@@ -29,6 +29,7 @@ export class RdsInstance extends Construct {
|
|
|
29
29
|
databaseProxySecurityGroup;
|
|
30
30
|
readReplicaSecurityGroup;
|
|
31
31
|
databaseNameValue;
|
|
32
|
+
backupRetentionDaysValue;
|
|
32
33
|
constructId;
|
|
33
34
|
constructor(scope, id, props) {
|
|
34
35
|
super(scope, id);
|
|
@@ -121,6 +122,11 @@ export class RdsInstance extends Construct {
|
|
|
121
122
|
const subnetType = props.publiclyAccessible
|
|
122
123
|
? SubnetType.PUBLIC
|
|
123
124
|
: SubnetType.PRIVATE_WITH_EGRESS;
|
|
125
|
+
const backupRetention = props.backupRetention ??
|
|
126
|
+
Duration.days(RDS_DEFAULTS.BACKUP_RETENTION_DEFAULT_DAYS);
|
|
127
|
+
// toDays() throws on a fractional Duration — same whole-days contract the
|
|
128
|
+
// L2 DatabaseInstance enforces, surfaced at synth either way.
|
|
129
|
+
this.backupRetentionDaysValue = backupRetention.toDays();
|
|
124
130
|
const commonInstanceProps = {
|
|
125
131
|
vpc: this.vpc,
|
|
126
132
|
vpcSubnets: {
|
|
@@ -130,8 +136,7 @@ export class RdsInstance extends Construct {
|
|
|
130
136
|
engine,
|
|
131
137
|
parameterGroup,
|
|
132
138
|
allocatedStorage: props.allocatedStorage,
|
|
133
|
-
backupRetention
|
|
134
|
-
Duration.days(RDS_DEFAULTS.BACKUP_RETENTION_DEFAULT_DAYS),
|
|
139
|
+
backupRetention,
|
|
135
140
|
preferredBackupWindow: props.preferredBackupWindow ?? "02:00-03:00",
|
|
136
141
|
storageEncrypted: true,
|
|
137
142
|
storageEncryptionKey,
|
|
@@ -156,7 +161,7 @@ export class RdsInstance extends Construct {
|
|
|
156
161
|
preferredMaintenanceWindow: props.preferredMaintenanceWindow ??
|
|
157
162
|
RDS_DEFAULTS.PREFERRED_MAINTENANCE_WINDOW,
|
|
158
163
|
publiclyAccessible: props.publiclyAccessible ?? false,
|
|
159
|
-
iamAuthentication: props.iamAuthentication
|
|
164
|
+
iamAuthentication: props.iamAuthentication ?? true
|
|
160
165
|
};
|
|
161
166
|
if (props.snapshotIdentifier) {
|
|
162
167
|
// Create from snapshot
|
|
@@ -333,7 +338,8 @@ exports.handler = async (event) => {
|
|
|
333
338
|
port: this.port,
|
|
334
339
|
deletionProtection: props.deletionProtection ?? true,
|
|
335
340
|
preferredMaintenanceWindow: props.preferredMaintenanceWindow ??
|
|
336
|
-
RDS_DEFAULTS.PREFERRED_MAINTENANCE_WINDOW
|
|
341
|
+
RDS_DEFAULTS.PREFERRED_MAINTENANCE_WINDOW,
|
|
342
|
+
iamAuthentication: props.iamAuthentication ?? true
|
|
337
343
|
});
|
|
338
344
|
readReplica.node.addDependency(deletionWaiter.resource);
|
|
339
345
|
}
|
|
@@ -357,6 +363,15 @@ exports.handler = async (event) => {
|
|
|
357
363
|
getSnapshotTarget() {
|
|
358
364
|
return { kind: "instance", arn: this.database.instanceArn };
|
|
359
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Effective automated-backup retention in whole days (the applied
|
|
368
|
+
* `backupRetention`, defaulted). Feeds `MIGRATION_SNAPSHOT_RETENTION_DAYS`
|
|
369
|
+
* so the migration runner prunes pre-migration snapshots against the same
|
|
370
|
+
* window the instance's PITR actually covers.
|
|
371
|
+
*/
|
|
372
|
+
getBackupRetentionDays() {
|
|
373
|
+
return this.backupRetentionDaysValue;
|
|
374
|
+
}
|
|
360
375
|
getDatabaseName() {
|
|
361
376
|
return this.databaseNameValue;
|
|
362
377
|
}
|
|
@@ -365,8 +380,8 @@ exports.handler = async (event) => {
|
|
|
365
380
|
}
|
|
366
381
|
/**
|
|
367
382
|
* Grant an IAM principal permission to connect as `dbUsername` via RDS IAM
|
|
368
|
-
* database authentication.
|
|
369
|
-
* `iamAuthentication:
|
|
383
|
+
* database authentication. Enabled by default; throws if the instance opted
|
|
384
|
+
* out with `iamAuthentication: false`. Delegates to the L2 `grantConnect`, which scopes
|
|
370
385
|
* `rds-db:connect` to the exact `dbuser:<dbiResourceId>/<dbUsername>` ARN —
|
|
371
386
|
* never a bare wildcard. See ADR
|
|
372
387
|
* decisions/2026-06-17-rls-role-auth-and-launch-gating.md.
|
|
@@ -2,7 +2,7 @@ import { Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
|
|
|
2
2
|
import * as CloudTrail from "aws-cdk-lib/aws-cloudtrail";
|
|
3
3
|
import { Construct } from "constructs";
|
|
4
4
|
import { CustomerManagedKey } from "../secrets/index.js";
|
|
5
|
-
import { S3Bucket } from "../storage/index.js";
|
|
5
|
+
import { NONCURRENT_VERSION_EXPIRY_DAYS, S3Bucket } from "../storage/index.js";
|
|
6
6
|
import { BucketEncryption } from "aws-cdk-lib/aws-s3";
|
|
7
7
|
import { PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
8
8
|
export function validateTrailProps(props) {
|
|
@@ -40,9 +40,18 @@ export class Trail extends Construct {
|
|
|
40
40
|
bucketKeyEnabled: true,
|
|
41
41
|
encryption: BucketEncryption.KMS,
|
|
42
42
|
encryptionKey: this.encryptionKey.key,
|
|
43
|
-
versioned:
|
|
43
|
+
versioned: true,
|
|
44
44
|
removalPolicy: storagePolicy,
|
|
45
|
-
|
|
45
|
+
// Custom lifecycleRules bypass S3Bucket's automatic
|
|
46
|
+
// noncurrentVersionExpiration pairing — carried explicitly, or
|
|
47
|
+
// noncurrent versions accumulate forever.
|
|
48
|
+
lifecycleRules: [
|
|
49
|
+
{
|
|
50
|
+
expiration: Duration.days(365),
|
|
51
|
+
noncurrentVersionExpiration: Duration.days(NONCURRENT_VERSION_EXPIRY_DAYS),
|
|
52
|
+
enabled: true
|
|
53
|
+
}
|
|
54
|
+
]
|
|
46
55
|
});
|
|
47
56
|
const effectiveTrailName = props.trailName || `${id}Trail`;
|
|
48
57
|
if (props.isOrganizationTrail === true) {
|
|
@@ -2,7 +2,7 @@ import { Construct } from "constructs";
|
|
|
2
2
|
import { Duration } from "aws-cdk-lib";
|
|
3
3
|
import { Rule } from "aws-cdk-lib/aws-events";
|
|
4
4
|
import { ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
5
|
-
import { SQSQueue } from "./sqs.js";
|
|
5
|
+
import { SQSQueue, SQS_LIMITS } from "./sqs.js";
|
|
6
6
|
import { resolveTarget } from "./eventTargets.js";
|
|
7
7
|
import { createScheduleAlarms } from "../monitoring/scheduleAlarms.js";
|
|
8
8
|
import { createSqsDlqAlarms } from "../monitoring/sqsAlarms.js";
|
|
@@ -69,7 +69,10 @@ export class EventBridgeRule extends Construct {
|
|
|
69
69
|
}
|
|
70
70
|
return {
|
|
71
71
|
queue: new SQSQueue(this, `${id}Dlq`, {
|
|
72
|
-
queueType: "standard"
|
|
72
|
+
queueType: "standard",
|
|
73
|
+
// Same 14-day window as the SQS wrapper's auto-DLQ — without it the
|
|
74
|
+
// provisioned DLQ falls back to the 4-day CloudFormation default.
|
|
75
|
+
messageRetentionPeriod: Duration.days(SQS_LIMITS.DEAD_LETTER_QUEUE.DEFAULT_RETENTION_DAYS).toSeconds()
|
|
73
76
|
}),
|
|
74
77
|
provisioned: true
|
|
75
78
|
};
|
|
@@ -22,6 +22,13 @@ export class SNSTopic extends Construct {
|
|
|
22
22
|
? (props.contentBasedDeduplication ?? true)
|
|
23
23
|
: undefined,
|
|
24
24
|
signatureVersion: props.signatureVersion ?? "2",
|
|
25
|
+
// The TLS-only deny synthesises an AWS::SNS::TopicPolicy, and that
|
|
26
|
+
// resource REPLACES the topic's implicit default policy — so any AWS
|
|
27
|
+
// service principal that published under the default Allow needs an
|
|
28
|
+
// explicit Allow merged in via getTopic().addToResourcePolicy(...)
|
|
29
|
+
// (SharedAlarmTopic carries the cloudwatch.amazonaws.com one).
|
|
30
|
+
// Same-account IAM publishers are unaffected: identity policies suffice.
|
|
31
|
+
enforceSSL: true,
|
|
25
32
|
masterKey: props.masterKey
|
|
26
33
|
});
|
|
27
34
|
// An SNS topic is a transient fan-out medium: it holds no durable state
|
|
@@ -118,6 +118,7 @@ export class SQSQueue extends Construct {
|
|
|
118
118
|
queueName: dlqName,
|
|
119
119
|
fifo: isFifo,
|
|
120
120
|
encryption: toEncryption(props.encryption),
|
|
121
|
+
enforceSSL: true,
|
|
121
122
|
retentionPeriod: Duration.days(SQS_LIMITS.DEAD_LETTER_QUEUE.DEFAULT_RETENTION_DAYS),
|
|
122
123
|
removalPolicy: resolvedRemovalPolicy
|
|
123
124
|
});
|
|
@@ -156,6 +157,7 @@ export class SQSQueue extends Construct {
|
|
|
156
157
|
: undefined,
|
|
157
158
|
deadLetterQueue,
|
|
158
159
|
encryption: toEncryption(props.encryption),
|
|
160
|
+
enforceSSL: true,
|
|
159
161
|
contentBasedDeduplication: isFifo
|
|
160
162
|
? (props.contentBasedDeduplication ?? true)
|
|
161
163
|
: undefined,
|
|
@@ -2,7 +2,7 @@ import { CfnOutput, Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
|
|
|
2
2
|
import * as ec2 from "aws-cdk-lib/aws-ec2";
|
|
3
3
|
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
4
4
|
import { LogGroup } from "../logging/logGroup.js";
|
|
5
|
-
import { S3Bucket } from "../storage/index.js";
|
|
5
|
+
import { NONCURRENT_VERSION_EXPIRY_DAYS, S3Bucket } from "../storage/index.js";
|
|
6
6
|
import { ResourceNaming } from "../../../utils/resourceNaming.js";
|
|
7
7
|
export const FLOW_LOG_TRAFFIC_TYPES = ["ALL", "ACCEPT", "REJECT"];
|
|
8
8
|
// Read at two sites (resources-layer constructor default + patterns-layer natGateways guard) that must agree.
|
|
@@ -183,8 +183,19 @@ export class Vpc extends ec2.Vpc {
|
|
|
183
183
|
const bucket = new S3Bucket(scope, `${id}FlowLogBucket`, {
|
|
184
184
|
bucketName: ResourceNaming.vpcFlowLogBucketName(id, Stack.of(scope).account),
|
|
185
185
|
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
186
|
+
versioned: true,
|
|
187
|
+
// Custom lifecycleRules bypass S3Bucket's automatic
|
|
188
|
+
// noncurrentVersionExpiration pairing — carried explicitly, or
|
|
189
|
+
// noncurrent versions accumulate forever. The no-retentionDays branch
|
|
190
|
+
// passes undefined, so the wrapper's own pairing applies there.
|
|
186
191
|
lifecycleRules: config.retentionDays
|
|
187
|
-
? [
|
|
192
|
+
? [
|
|
193
|
+
{
|
|
194
|
+
expiration: Duration.days(config.retentionDays),
|
|
195
|
+
noncurrentVersionExpiration: Duration.days(NONCURRENT_VERSION_EXPIRY_DAYS),
|
|
196
|
+
enabled: true
|
|
197
|
+
}
|
|
198
|
+
]
|
|
188
199
|
: undefined
|
|
189
200
|
});
|
|
190
201
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { aws_ssm as ssm, Stack } from "aws-cdk-lib";
|
|
1
|
+
import { Annotations, aws_ssm as ssm, Stack } from "aws-cdk-lib";
|
|
2
2
|
import { PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
3
3
|
import { AwsCustomResourcePolicy, PhysicalResourceId } from "aws-cdk-lib/custom-resources";
|
|
4
4
|
import { Construct } from "constructs";
|
|
@@ -20,6 +20,12 @@ export class SecureStringParameter extends Construct {
|
|
|
20
20
|
constructor(scope, id, props) {
|
|
21
21
|
super(scope, id);
|
|
22
22
|
this.name = props.name;
|
|
23
|
+
// Matches the || fall-through below: an empty-string value or env var
|
|
24
|
+
// also lands on the placeholder.
|
|
25
|
+
const resolvedValue = props.value || process.env[`CDK_SECURE_STRING_${id}`];
|
|
26
|
+
if (!resolvedValue) {
|
|
27
|
+
Annotations.of(this).addWarningV2("@fjall/components-infrastructure:secrets:secureStringPlaceholderValue", `SecureStringParameter '${props.name}' has no value source — the well-known literal 'placeholderValue' will be written to the SecureString on create and update, and consumers will silently read it. Supply a real value via the 'value' prop or the CDK_SECURE_STRING_${id} environment variable at synth time.`);
|
|
28
|
+
}
|
|
23
29
|
if (props.cmk) {
|
|
24
30
|
this.cmk = props.cmk;
|
|
25
31
|
}
|
|
@@ -39,9 +45,7 @@ export class SecureStringParameter extends Construct {
|
|
|
39
45
|
parameters: {
|
|
40
46
|
Name: props.name,
|
|
41
47
|
Description: props.description || `${id} secure parameter`,
|
|
42
|
-
Value:
|
|
43
|
-
process.env[`CDK_SECURE_STRING_${id}`] ||
|
|
44
|
-
"placeholderValue",
|
|
48
|
+
Value: resolvedValue || "placeholderValue",
|
|
45
49
|
Type: "SecureString",
|
|
46
50
|
KeyId: this.cmk.alias.keyId
|
|
47
51
|
// TODO: Add tags to the parameter
|
|
@@ -54,9 +58,7 @@ export class SecureStringParameter extends Construct {
|
|
|
54
58
|
parameters: {
|
|
55
59
|
Name: props.name,
|
|
56
60
|
Description: props.description || `${id} secure parameter`,
|
|
57
|
-
Value:
|
|
58
|
-
process.env[`CDK_SECURE_STRING_${id}`] ||
|
|
59
|
-
"placeholderValue",
|
|
61
|
+
Value: resolvedValue || "placeholderValue",
|
|
60
62
|
Overwrite: props.overwrite,
|
|
61
63
|
Type: "SecureString",
|
|
62
64
|
KeyId: this.cmk.alias.keyId
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SecretValue } from "aws-cdk-lib";
|
|
1
|
+
import { Annotations, SecretValue } from "aws-cdk-lib";
|
|
2
2
|
import { Secret as CdkSecret } from "aws-cdk-lib/aws-secretsmanager";
|
|
3
3
|
import { Construct } from "constructs";
|
|
4
4
|
import { CustomerManagedKey } from "./kms.js";
|
|
@@ -81,6 +81,10 @@ export class Secret extends Construct {
|
|
|
81
81
|
/**
|
|
82
82
|
* If a secretStringValue is provided, use it to create the secret.
|
|
83
83
|
*/
|
|
84
|
+
if (props.secretStringValue) {
|
|
85
|
+
// Gotcha: never interpolate the secret value into this message.
|
|
86
|
+
Annotations.of(this).addWarningV2("@fjall/components-infrastructure:secrets:plainTextSecretValue", `Secret '${id}' uses secretStringValue — the plain-text value is embedded verbatim in the synthesised CloudFormation template and every synth artefact (SecretValue.unsafePlainText). Prefer generateSecretString (server-generated, never leaves AWS) or secretObjectValue with SecretValue references (e.g. SecretValue.ssmSecure).`);
|
|
87
|
+
}
|
|
84
88
|
const secretStringValue = props.secretStringValue
|
|
85
89
|
? {
|
|
86
90
|
secretStringValue: SecretValue.unsafePlainText(props.secretStringValue)
|
|
@@ -2,6 +2,14 @@ import { Bucket, type BucketProps } from "aws-cdk-lib/aws-s3";
|
|
|
2
2
|
import { type Construct } from "constructs";
|
|
3
3
|
import { type BackupTier } from "../../../utils/backupTierMapping.js";
|
|
4
4
|
export { SDK_PRE_EMPTY_TAG_KEY } from "@fjall/util/aws";
|
|
5
|
+
/**
|
|
6
|
+
* Days a noncurrent object version is retained before lifecycle expiry.
|
|
7
|
+
* The wrapper pairs this automatically when it enables versioning; constructs
|
|
8
|
+
* passing custom `lifecycleRules` (cloudTrail, configRecorder, vpc flow logs)
|
|
9
|
+
* bypass that pairing and must carry the same value explicitly — coupled at
|
|
10
|
+
* every site so the retention window cannot drift.
|
|
11
|
+
*/
|
|
12
|
+
export declare const NONCURRENT_VERSION_EXPIRY_DAYS = 30;
|
|
5
13
|
export interface WebsiteHostingConfig {
|
|
6
14
|
readonly indexDocument: string;
|
|
7
15
|
readonly errorDocument?: string;
|
|
@@ -31,6 +39,11 @@ export interface ResourcePolicyStatement {
|
|
|
31
39
|
* `autoDeleteObjects` is accepted for source compatibility but always
|
|
32
40
|
* overridden to `false` (ADR D4.3) — passing `true` raises a synth-time
|
|
33
41
|
* warning rather than a compile error so older scaffolds keep building.
|
|
42
|
+
*
|
|
43
|
+
* Non-public buckets assert `BlockPublicAccess.BLOCK_ALL` in the template
|
|
44
|
+
* (caller `blockPublicAccess` wins); public/website buckets keep the two
|
|
45
|
+
* policy halves open but block the ACL halves. KMS-encrypted buckets default
|
|
46
|
+
* `bucketKeyEnabled: true` (pass `false` to opt out).
|
|
34
47
|
*/
|
|
35
48
|
export interface S3BucketProps extends BucketProps {
|
|
36
49
|
backupVaultTier?: BackupTier;
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { SDK_PRE_EMPTY_TAG_KEY } from "@fjall/util/aws";
|
|
2
2
|
import { Annotations, CfnOutput, Duration, RemovalPolicy, Tags } from "aws-cdk-lib";
|
|
3
|
-
import { BlockPublicAccess, Bucket } from "aws-cdk-lib/aws-s3";
|
|
3
|
+
import { BlockPublicAccess, Bucket, BucketEncryption } from "aws-cdk-lib/aws-s3";
|
|
4
4
|
import { ArnPrincipal, Effect, PolicyStatement, StarPrincipal } from "aws-cdk-lib/aws-iam";
|
|
5
5
|
import { RegionInfo } from "aws-cdk-lib/region-info";
|
|
6
6
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
7
7
|
import { bucketWebsiteEndpointExportName, bucketWebsiteHostedZoneIdExportName } from "@fjall/util";
|
|
8
8
|
import { envAwareRemovalPolicyDefault, toRemovalPolicy } from "../../../utils/removalPolicy.js";
|
|
9
9
|
export { SDK_PRE_EMPTY_TAG_KEY } from "@fjall/util/aws";
|
|
10
|
+
/**
|
|
11
|
+
* Days a noncurrent object version is retained before lifecycle expiry.
|
|
12
|
+
* The wrapper pairs this automatically when it enables versioning; constructs
|
|
13
|
+
* passing custom `lifecycleRules` (cloudTrail, configRecorder, vpc flow logs)
|
|
14
|
+
* bypass that pairing and must carry the same value explicitly — coupled at
|
|
15
|
+
* every site so the retention window cannot drift.
|
|
16
|
+
*/
|
|
17
|
+
export const NONCURRENT_VERSION_EXPIRY_DAYS = 30;
|
|
10
18
|
function shouldAutoVersion(tier) {
|
|
11
19
|
return tier === "resilient" || tier === "enterprise";
|
|
12
20
|
}
|
|
@@ -22,6 +30,12 @@ export class S3Bucket extends Bucket {
|
|
|
22
30
|
const isPublic = props.publicReadAccess === true || websiteHosting !== undefined;
|
|
23
31
|
const versioned = props.versioned ?? shouldAutoVersion(backupVaultTier);
|
|
24
32
|
const removalPolicy = props.removalPolicy ?? toRemovalPolicy(envAwareRemovalPolicyDefault());
|
|
33
|
+
// Mirrors the CDK Bucket's own resolution: an encryptionKey with no
|
|
34
|
+
// explicit encryption infers KMS. DSSE is excluded deliberately — S3
|
|
35
|
+
// Bucket Keys are not supported for dual-layer SSE-KMS.
|
|
36
|
+
const kmsEncryption = props.encryption === BucketEncryption.KMS ||
|
|
37
|
+
props.encryption === BucketEncryption.KMS_MANAGED ||
|
|
38
|
+
(props.encryption === undefined && props.encryptionKey !== undefined);
|
|
25
39
|
super(scope, id, {
|
|
26
40
|
...cdkProps,
|
|
27
41
|
enforceSSL: true,
|
|
@@ -31,14 +45,22 @@ export class S3Bucket extends Bucket {
|
|
|
31
45
|
autoDeleteObjects: false,
|
|
32
46
|
removalPolicy,
|
|
33
47
|
publicReadAccess: isPublic,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
// Public access here is bucket-policy-based, so only the policy halves
|
|
49
|
+
// stay open (grantPublicAccess validates against blockPublicPolicy
|
|
50
|
+
// alone); the ACL halves close — ACLs are disabled on modern buckets.
|
|
51
|
+
// Non-public buckets assert BLOCK_ALL in the template rather than
|
|
52
|
+
// relying on the creation-time account default, so a manually loosened
|
|
53
|
+
// bucket is healed on the next deploy.
|
|
54
|
+
blockPublicAccess: isPublic
|
|
55
|
+
? new BlockPublicAccess({
|
|
56
|
+
blockPublicAcls: true,
|
|
37
57
|
blockPublicPolicy: false,
|
|
38
|
-
ignorePublicAcls:
|
|
58
|
+
ignorePublicAcls: true,
|
|
39
59
|
restrictPublicBuckets: false
|
|
40
60
|
})
|
|
41
|
-
|
|
61
|
+
: (props.blockPublicAccess ?? BlockPublicAccess.BLOCK_ALL),
|
|
62
|
+
...(props.bucketKeyEnabled === undefined &&
|
|
63
|
+
kmsEncryption && { bucketKeyEnabled: true }),
|
|
42
64
|
...(websiteHosting && {
|
|
43
65
|
websiteIndexDocument: websiteHosting.indexDocument,
|
|
44
66
|
websiteErrorDocument: websiteHosting.errorDocument ?? "error.html"
|
|
@@ -54,7 +76,12 @@ export class S3Bucket extends Bucket {
|
|
|
54
76
|
enabled: true
|
|
55
77
|
},
|
|
56
78
|
...(versioned && !props.lifecycleRules
|
|
57
|
-
? [
|
|
79
|
+
? [
|
|
80
|
+
{
|
|
81
|
+
noncurrentVersionExpiration: Duration.days(NONCURRENT_VERSION_EXPIRY_DAYS),
|
|
82
|
+
enabled: true
|
|
83
|
+
}
|
|
84
|
+
]
|
|
58
85
|
: (props.lifecycleRules ?? []))
|
|
59
86
|
]
|
|
60
87
|
});
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { Stack } from "aws-cdk-lib";
|
|
2
2
|
import { Project } from "aws-cdk-lib/aws-codebuild";
|
|
3
3
|
import { Construct } from "constructs";
|
|
4
|
+
import { DEFAULT_FRAMEWORK_LOG_RETENTION, LogGroup } from "../logging/logGroup.js";
|
|
4
5
|
export class CodeBuildProject extends Construct {
|
|
5
6
|
project;
|
|
6
7
|
constructor(scope, id, props) {
|
|
7
8
|
super(scope, id);
|
|
8
9
|
this.project = new Project(this, id, {
|
|
9
10
|
...props,
|
|
10
|
-
description: props.description || `Code build project for ${id}
|
|
11
|
+
description: props.description || `Code build project for ${id}`,
|
|
12
|
+
// Without a logging prop CodeBuild auto-creates a never-expiring
|
|
13
|
+
// runtime log group outside CloudFormation; default to a framework
|
|
14
|
+
// group so retention and removal policy are governed.
|
|
15
|
+
logging: props.logging ?? {
|
|
16
|
+
cloudWatch: {
|
|
17
|
+
logGroup: new LogGroup(this, `${id}LogGroup`, {
|
|
18
|
+
retention: DEFAULT_FRAMEWORK_LOG_RETENTION
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
}
|
|
11
22
|
});
|
|
12
23
|
}
|
|
13
24
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "@fjall/util/aws";
|
|
1
|
+
import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT, DEFAULT_COST_ALLOCATION_OWNER } from "@fjall/util/aws";
|
|
2
2
|
import type { IConstruct } from "constructs";
|
|
3
|
-
export { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT };
|
|
3
|
+
export { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT, DEFAULT_COST_ALLOCATION_OWNER };
|
|
4
4
|
export interface CostAllocationTagsArgs {
|
|
5
5
|
readonly service: string;
|
|
6
6
|
readonly domain: string;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "@fjall/util/aws";
|
|
1
|
+
import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT, DEFAULT_COST_ALLOCATION_OWNER } from "@fjall/util/aws";
|
|
2
2
|
import { Tags } from "aws-cdk-lib";
|
|
3
3
|
// Canonical home: @fjall/util (`util/src/aws/costAllocationTags.ts`) — shared
|
|
4
4
|
// with deploy-core's `cdk bootstrap --tags` stamping. Re-exported here so
|
|
5
5
|
// construct-package consumers keep their existing import path.
|
|
6
|
-
export { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT };
|
|
6
|
+
export { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT, DEFAULT_COST_ALLOCATION_OWNER };
|
|
7
7
|
export function applyCostAllocationTags(scope, args) {
|
|
8
8
|
if (args.environment !== undefined) {
|
|
9
9
|
Tags.of(scope).add(COST_ALLOCATION_TAGS.ENVIRONMENT, args.environment);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "28.0.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/fjall-tech/fjall.git",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"@aws-sdk/client-organizations": "^3.1098.0",
|
|
81
|
-
"@fjall/generator": "^
|
|
82
|
-
"@fjall/util": "^
|
|
81
|
+
"@fjall/generator": "^28.0.0",
|
|
82
|
+
"@fjall/util": "^28.0.0",
|
|
83
83
|
"constructs": "^10.7.2",
|
|
84
84
|
"zod": "^4.4.3"
|
|
85
85
|
},
|