@fjall/components-infrastructure 37.0.0 → 39.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/app.d.ts +52 -12
- package/dist/lib/app.js +60 -24
- package/dist/lib/config/aws/configRecorder.js +3 -0
- package/dist/lib/config/aws/disasterRecovery.d.ts +1 -2
- package/dist/lib/config/aws/disasterRecovery.js +167 -45
- package/dist/lib/patterns/aws/buildkite/buildkite.js +7 -2
- package/dist/lib/patterns/aws/cdn.d.ts +19 -11
- package/dist/lib/patterns/aws/cdn.js +53 -25
- package/dist/lib/patterns/aws/clickhouseDatabase.js +8 -4
- package/dist/lib/patterns/aws/computeEcs.js +17 -1
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +30 -4
- package/dist/lib/patterns/aws/computePropApplicability.js +1 -0
- package/dist/lib/patterns/aws/domain.js +3 -3
- package/dist/lib/patterns/aws/externalRecordsPattern.d.ts +9 -6
- package/dist/lib/patterns/aws/externalRecordsPattern.js +22 -46
- package/dist/lib/patterns/aws/interfaces/domain.d.ts +8 -5
- package/dist/lib/patterns/aws/interfaces/pattern.d.ts +9 -10
- package/dist/lib/patterns/aws/messaging.d.ts +10 -0
- package/dist/lib/patterns/aws/messaging.js +10 -6
- package/dist/lib/patterns/aws/payload.d.ts +5 -0
- package/dist/lib/patterns/aws/payload.js +23 -11
- package/dist/lib/patterns/aws/storage.d.ts +41 -0
- package/dist/lib/patterns/aws/storage.js +23 -8
- package/dist/lib/patterns/aws/targets/targetResolution.d.ts +4 -4
- package/dist/lib/patterns/aws/targets/targetResolution.js +4 -4
- package/dist/lib/resources/aws/backup/backupVault.d.ts +27 -2
- package/dist/lib/resources/aws/backup/backupVault.js +49 -6
- package/dist/lib/resources/aws/cdn/cloudFront.d.ts +42 -18
- package/dist/lib/resources/aws/cdn/cloudFront.js +39 -51
- package/dist/lib/resources/aws/cdn/cloudFrontLogDelivery.d.ts +90 -0
- package/dist/lib/resources/aws/cdn/cloudFrontLogDelivery.js +213 -0
- package/dist/lib/resources/aws/cdn/index.d.ts +1 -0
- package/dist/lib/resources/aws/cdn/index.js +1 -0
- package/dist/lib/resources/aws/compute/ecs.d.ts +7 -4
- package/dist/lib/resources/aws/compute/ecs.js +27 -21
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +3 -0
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +11 -0
- package/dist/lib/resources/aws/compute/ecsValidation.d.ts +8 -0
- package/dist/lib/resources/aws/compute/ecsValidation.js +8 -0
- package/dist/lib/resources/aws/compute/persistentDataVolume.d.ts +8 -0
- package/dist/lib/resources/aws/compute/persistentDataVolume.js +2 -0
- package/dist/lib/resources/aws/database/dynamodb.js +2 -1
- package/dist/lib/resources/aws/database/rdsAurora.js +6 -1
- package/dist/lib/resources/aws/database/rdsInstance.js +6 -1
- package/dist/lib/resources/aws/logging/accessLogBucket.d.ts +15 -8
- package/dist/lib/resources/aws/logging/accessLogBucket.js +16 -9
- package/dist/lib/resources/aws/logging/cloudTrail.js +3 -0
- package/dist/lib/resources/aws/messaging/eventBridgeQueueGrants.d.ts +16 -0
- package/dist/lib/resources/aws/messaging/eventBridgeQueueGrants.js +91 -0
- package/dist/lib/resources/aws/messaging/eventBridgeRule.js +36 -5
- package/dist/lib/resources/aws/messaging/eventTargets.d.ts +4 -3
- package/dist/lib/resources/aws/messaging/eventTargets.js +71 -31
- package/dist/lib/resources/aws/messaging/queuePolicyQuota.d.ts +25 -0
- package/dist/lib/resources/aws/messaging/queuePolicyQuota.js +190 -0
- package/dist/lib/resources/aws/messaging/schedule.d.ts +5 -5
- package/dist/lib/resources/aws/messaging/sqs.d.ts +36 -1
- package/dist/lib/resources/aws/messaging/sqs.js +40 -2
- package/dist/lib/resources/aws/messaging/subscription.d.ts +4 -3
- package/dist/lib/resources/aws/networking/vpc.js +12 -6
- package/dist/lib/resources/aws/secrets/stackEncryptionKey.d.ts +26 -1
- package/dist/lib/resources/aws/secrets/stackEncryptionKey.js +56 -1
- package/dist/lib/resources/aws/storage/s3.d.ts +21 -1
- package/dist/lib/resources/aws/storage/s3.js +89 -3
- package/dist/lib/utils/backupTierMapping.d.ts +23 -2
- package/dist/lib/utils/backupTierMapping.js +25 -0
- package/dist/lib/utils/cdkContext.d.ts +14 -0
- package/dist/lib/utils/cdkContext.js +38 -1
- package/dist/lib/utils/exportNaming.d.ts +30 -27
- package/dist/lib/utils/exportNaming.js +32 -28
- package/dist/lib/utils/standardTagsAspect.d.ts +8 -18
- package/dist/lib/utils/standardTagsAspect.js +8 -34
- package/package.json +4 -4
|
@@ -95,8 +95,13 @@ export class Buildkite extends Construct {
|
|
|
95
95
|
appBackupTier
|
|
96
96
|
});
|
|
97
97
|
// Secrets (SSH deploy keys, git credentials) must survive stack teardown
|
|
98
|
-
// and accidental overwrite
|
|
99
|
-
|
|
98
|
+
// and accidental overwrite — which is exactly what a backup plan is for,
|
|
99
|
+
// so the bucket takes the app's tier like the artefact bucket above.
|
|
100
|
+
const managedSecretsBucket = new S3Bucket(this, `${id}ManagedSecretsBucket`, {
|
|
101
|
+
versioned: true,
|
|
102
|
+
removalPolicy: RemovalPolicy.RETAIN,
|
|
103
|
+
appBackupTier
|
|
104
|
+
});
|
|
100
105
|
if (fjallApiKeyParameterName !== undefined ||
|
|
101
106
|
pipelineSecretsPrefix !== undefined) {
|
|
102
107
|
this.addAgentSecretsEnvHook(id, managedSecretsBucket, {
|
|
@@ -4,7 +4,7 @@ import { type IApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalanc
|
|
|
4
4
|
import { type ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
5
5
|
import { type ICachePolicy, type IResponseHeadersPolicy } from "aws-cdk-lib/aws-cloudfront";
|
|
6
6
|
import type App from "../../app.js";
|
|
7
|
-
import { CloudFrontDistribution, type CachePolicyPreset, type AccessGateConfig } from "../../resources/aws/cdn/index.js";
|
|
7
|
+
import { CloudFrontDistribution, type CloudFrontLogFormat, type CachePolicyPreset, type AccessGateConfig } from "../../resources/aws/cdn/index.js";
|
|
8
8
|
import { type ICdn } from "./interfaces/cdn.js";
|
|
9
9
|
import { type StaticSiteRouting, type CloudFrontPriceClass } from "@fjall/util";
|
|
10
10
|
import { type BackupTier } from "../../utils/backupTierMapping.js";
|
|
@@ -83,17 +83,17 @@ interface BaseCdnProps {
|
|
|
83
83
|
certificateArn?: string;
|
|
84
84
|
comment?: string;
|
|
85
85
|
/**
|
|
86
|
-
* Standard (
|
|
87
|
-
* `cloudFrontLogging` column — enterprise
|
|
88
|
-
* lifecycle-expired access-log bucket under
|
|
89
|
-
*
|
|
90
|
-
* `
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
86
|
+
* Standard logging (v2): access logs delivered through CloudWatch Logs.
|
|
87
|
+
* Default: the app backup tier's `cloudFrontLogging` column — enterprise
|
|
88
|
+
* logs to the stack's shared, lifecycle-expired access-log bucket under
|
|
89
|
+
* `cloudfront/<id>`, as JSON. An object takes over at any tier (its
|
|
90
|
+
* `bucket`, `prefix` and `format` each default as above); `false` opts
|
|
91
|
+
* out at any tier. The delivery resources live in the CDN stack when the
|
|
92
|
+
* app deploys to us-east-1 and in the app's `UsEast1CdnLogging` stack
|
|
93
|
+
* otherwise (CloudWatch Logs accepts them there only), so a logging CDN
|
|
94
|
+
* outside us-east-1 needs the `App` — the factory path fills it.
|
|
94
95
|
*/
|
|
95
|
-
|
|
96
|
-
logBucket?: IBucket;
|
|
96
|
+
logging?: CdnLoggingConfig | false;
|
|
97
97
|
priceClass?: CloudFrontPriceClass;
|
|
98
98
|
forwardHostHeader?: boolean;
|
|
99
99
|
accessGate?: false | AccessGateConfig;
|
|
@@ -104,6 +104,14 @@ interface BaseCdnProps {
|
|
|
104
104
|
*/
|
|
105
105
|
appBackupTier?: BackupTier;
|
|
106
106
|
}
|
|
107
|
+
export interface CdnLoggingConfig {
|
|
108
|
+
/** Bucket the logs land in; defaults to the stack's access-log bucket. */
|
|
109
|
+
bucket?: IBucket;
|
|
110
|
+
/** Key prefix (no leading or trailing slash); defaults to `cloudfront/<id>`. */
|
|
111
|
+
prefix?: string;
|
|
112
|
+
/** CloudWatch Logs output format; defaults to `json`. */
|
|
113
|
+
format?: CloudFrontLogFormat;
|
|
114
|
+
}
|
|
107
115
|
/**
|
|
108
116
|
* S3 origin CDN props - use with S3 bucket
|
|
109
117
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Fn, Token } from "aws-cdk-lib";
|
|
1
|
+
import { Fn, Stack, Token } from "aws-cdk-lib";
|
|
2
2
|
import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
3
3
|
import { CloudFrontTarget, LoadBalancerTarget } from "aws-cdk-lib/aws-route53-targets";
|
|
4
|
-
import { CloudFrontDistribution } from "../../resources/aws/cdn/index.js";
|
|
4
|
+
import { assertNoLegacyLoggingProps, CLOUDFRONT_LOG_DELIVERY_REGION, CloudFrontDistribution } from "../../resources/aws/cdn/index.js";
|
|
5
5
|
import { AliasRecord } from "../../resources/aws/networking/index.js";
|
|
6
6
|
import { tierDefault } from "../../utils/backupTierMapping.js";
|
|
7
7
|
import { stackAccessLogBucket } from "../../resources/aws/logging/index.js";
|
|
@@ -12,31 +12,54 @@ import { isStorage } from "./storage.js";
|
|
|
12
12
|
import { CdnEcsOriginResolver, cdnOriginRefusal, internalAlbMessage, normaliseDnsName } from "./cdnAppOrigin.js";
|
|
13
13
|
import { isCompute, isEcsCompute, isLambdaCompute } from "./compute.js";
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* to the stack's shared access-log bucket under its own prefix.
|
|
15
|
+
* `false` is off at any tier; an object is on at any tier with its own
|
|
16
|
+
* defaults filled; `undefined` lets the tier decide, and a tier-enabled CDN
|
|
17
|
+
* logs to the stack's shared access-log bucket under its own prefix.
|
|
18
18
|
*/
|
|
19
19
|
function resolveCdnLogging(scope, id, props) {
|
|
20
|
-
if (props.
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
if (!tierDefault(props.appBackupTier, "cloudFrontLogging")) {
|
|
28
|
-
return {
|
|
29
|
-
enableLogging: undefined,
|
|
30
|
-
logBucket: undefined,
|
|
31
|
-
logFilePrefix: undefined
|
|
32
|
-
};
|
|
20
|
+
if (props.logging === false)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (props.logging === undefined &&
|
|
23
|
+
!tierDefault(props.appBackupTier, "cloudFrontLogging")) {
|
|
24
|
+
return undefined;
|
|
33
25
|
}
|
|
26
|
+
const config = props.logging ?? {};
|
|
34
27
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
bucket: config.bucket ?? stackAccessLogBucket(scope),
|
|
29
|
+
...(config.prefix !== undefined && { prefix: config.prefix }),
|
|
30
|
+
...(config.format !== undefined && { format: config.format }),
|
|
31
|
+
...resolveLogDeliveryScope(scope, id, props.app)
|
|
38
32
|
};
|
|
39
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* CloudWatch Logs accepts CloudFront delivery resources in us-east-1 only.
|
|
36
|
+
* A CDN stack already there hosts them itself; any other region hands them
|
|
37
|
+
* to the app's `UsEast1CdnLogging` stack, whose distribution and bucket
|
|
38
|
+
* ARNs arrive through CDK cross-region references — which need both stacks
|
|
39
|
+
* env-pinned, so a token region is refused here with the cure rather than
|
|
40
|
+
* by CloudFormation with a region error after the CDN stack has deployed.
|
|
41
|
+
*/
|
|
42
|
+
function resolveLogDeliveryScope(scope, id, app) {
|
|
43
|
+
const region = Stack.of(scope).region;
|
|
44
|
+
if (Token.isUnresolved(region)) {
|
|
45
|
+
throw new Error(`CDN '${id}': standard logging (v2) needs a concrete region at synth ` +
|
|
46
|
+
"— the delivery resources go to us-east-1 and reach the " +
|
|
47
|
+
"distribution through cross-region references, which resolve only " +
|
|
48
|
+
"between env-pinned stacks — but this stack's region is a token. " +
|
|
49
|
+
"Deploy through the Fjall CLI, or export CDK_DEFAULT_ACCOUNT and " +
|
|
50
|
+
"CDK_DEFAULT_REGION before synthesising; or set logging: false.");
|
|
51
|
+
}
|
|
52
|
+
if (region === CLOUDFRONT_LOG_DELIVERY_REGION)
|
|
53
|
+
return {};
|
|
54
|
+
if (app === undefined) {
|
|
55
|
+
throw new Error(`CDN '${id}': standard logging (v2) from region ${region} places its ` +
|
|
56
|
+
"delivery resources in the app's us-east-1 logging stack, but 'app' " +
|
|
57
|
+
"is unset. Create the CDN through the factory path — " +
|
|
58
|
+
"app.addCdn(CdnFactory.build(...)) — which fills it, pass 'app' " +
|
|
59
|
+
"explicitly, or set logging: false.");
|
|
60
|
+
}
|
|
61
|
+
return { deliveryScope: app.getUsEast1CdnLoggingStack().getStack() };
|
|
62
|
+
}
|
|
40
63
|
/**
|
|
41
64
|
* The distribution's own alias names, normalised — consumed by the
|
|
42
65
|
* origin-loop guard and by the ECS origin resolver (auto-resolution excludes
|
|
@@ -59,7 +82,10 @@ function collectAliasNames(props) {
|
|
|
59
82
|
* Validates CDN props — synth-time hard errors for shapes CloudFormation
|
|
60
83
|
* would only reject at deploy.
|
|
61
84
|
*/
|
|
62
|
-
function validateCdnProps(props) {
|
|
85
|
+
function validateCdnProps(id, props) {
|
|
86
|
+
// The pattern rebuilds the distribution props field by field, so the
|
|
87
|
+
// resource-layer refusal never sees a legacy key given here.
|
|
88
|
+
assertNoLegacyLoggingProps(`CDN '${id}'`, props, "logging: { bucket?, prefix?, format? } | false");
|
|
63
89
|
const hasCertificate = !!(props.certificate || props.certificateArn);
|
|
64
90
|
// domainConfig is the managed-domain authority for the viewer config —
|
|
65
91
|
// mixing it with the literal surface would give the alias list and the
|
|
@@ -251,7 +277,7 @@ const ROUTING_CDN_BEHAVIOUR = {
|
|
|
251
277
|
};
|
|
252
278
|
export class Cdn extends CloudFrontDistribution {
|
|
253
279
|
constructor(scope, id, props) {
|
|
254
|
-
validateCdnProps(props);
|
|
280
|
+
validateCdnProps(id, props);
|
|
255
281
|
// Zone + certificate resolve BEFORE super() — the viewer certificate is
|
|
256
282
|
// part of the distribution's props — so their constructs land on the CDN
|
|
257
283
|
// stack (`scope`) under `${id}`-prefixed IDs, the pattern convention.
|
|
@@ -476,6 +502,7 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
476
502
|
originRequestPolicy: props.originRequestPolicy
|
|
477
503
|
}
|
|
478
504
|
: {};
|
|
505
|
+
const logging = resolveCdnLogging(scope, id, props);
|
|
479
506
|
return {
|
|
480
507
|
distributionProps: {
|
|
481
508
|
appName,
|
|
@@ -486,7 +513,7 @@ export class Cdn extends CloudFrontDistribution {
|
|
|
486
513
|
domainNames,
|
|
487
514
|
certificate,
|
|
488
515
|
comment: props.comment,
|
|
489
|
-
...
|
|
516
|
+
...(logging !== undefined && { logging }),
|
|
490
517
|
priceClass: props.priceClass,
|
|
491
518
|
forwardHostHeader: props.forwardHostHeader ?? impliedForwardHostHeader,
|
|
492
519
|
accessGate: props.accessGate,
|
|
@@ -699,7 +726,8 @@ export class CdnFactory {
|
|
|
699
726
|
// The App is the only place the app name is known here; without it the
|
|
700
727
|
// export falls back to the construct id and the Domain import misses.
|
|
701
728
|
// The App instance itself rides along for domainConfig resolution
|
|
702
|
-
// (certificate placement needs app.getUsEast1CertificateStack())
|
|
729
|
+
// (certificate placement needs app.getUsEast1CertificateStack()) and
|
|
730
|
+
// for log-delivery placement (app.getUsEast1CdnLoggingStack()).
|
|
703
731
|
return new Cdn(scope, id, {
|
|
704
732
|
...props,
|
|
705
733
|
appName: props.appName ?? app.getName(),
|
|
@@ -367,6 +367,9 @@ export class ClickHouseDatabase extends Construct {
|
|
|
367
367
|
const backupBucket = props.backupBucket ??
|
|
368
368
|
new S3Bucket(this, "ClickHouseBackupBucket", {
|
|
369
369
|
versioned: true,
|
|
370
|
+
...(props.appBackupTier !== undefined && {
|
|
371
|
+
appBackupTier: props.appBackupTier
|
|
372
|
+
}),
|
|
370
373
|
lifecycleRules: [
|
|
371
374
|
{
|
|
372
375
|
enabled: true,
|
|
@@ -525,8 +528,7 @@ export class ClickHouseDatabase extends Construct {
|
|
|
525
528
|
`${sidecarTlsPreamble}${optimisePreamble}\n${optimiseScript}`
|
|
526
529
|
],
|
|
527
530
|
secrets: maintenanceSecrets,
|
|
528
|
-
logGroup: optimiseTaskLogGroup
|
|
529
|
-
securityGroups: [securityGroup]
|
|
531
|
+
logGroup: optimiseTaskLogGroup
|
|
530
532
|
});
|
|
531
533
|
maintenanceLogGroups.push({
|
|
532
534
|
job: "optimise",
|
|
@@ -555,8 +557,7 @@ export class ClickHouseDatabase extends Construct {
|
|
|
555
557
|
})}`
|
|
556
558
|
],
|
|
557
559
|
secrets: maintenanceSecrets,
|
|
558
|
-
logGroup: backupTaskLogGroup
|
|
559
|
-
securityGroups: [securityGroup]
|
|
560
|
+
logGroup: backupTaskLogGroup
|
|
560
561
|
});
|
|
561
562
|
maintenanceLogGroups.push({
|
|
562
563
|
job: "backup",
|
|
@@ -590,6 +591,9 @@ export class ClickHouseDatabase extends Construct {
|
|
|
590
591
|
...(props.appBackupTier !== undefined && {
|
|
591
592
|
appBackupTier: props.appBackupTier
|
|
592
593
|
}),
|
|
594
|
+
// ClickHouse is infrastructure: the deploy engine must never roll it
|
|
595
|
+
// out, restart it or rebind it as one of the app's own services.
|
|
596
|
+
deployableServices: false,
|
|
593
597
|
cluster: {
|
|
594
598
|
loadBalancer: false,
|
|
595
599
|
securityGroup,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AwsLogDriver, ContainerImage, DeploymentLifecycleStage, Secret as EcsSecret } from "aws-cdk-lib/aws-ecs";
|
|
1
|
+
import { AwsLogDriver, ContainerImage, DeploymentLifecycleStage, NetworkMode, Secret as EcsSecret } from "aws-cdk-lib/aws-ecs";
|
|
2
2
|
import { Peer, Port, SubnetType } from "aws-cdk-lib/aws-ec2";
|
|
3
3
|
import { Effect, PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import { Annotations, Aws, Stack, Token } from "aws-cdk-lib";
|
|
@@ -269,6 +269,21 @@ export function validateEcsProps(props) {
|
|
|
269
269
|
"does not own. Either omit cluster.securityGroup (a Fjall-owned SG will be created) or set directAccess: false.");
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
|
+
for (const entry of props.cluster?.scheduledTasks ?? []) {
|
|
273
|
+
if (entry.networkMode === NetworkMode.AWS_VPC) {
|
|
274
|
+
throw new Error(`Scheduled task '${entry.name}': networkMode AWS_VPC is not supported for scheduled tasks — ` +
|
|
275
|
+
"a scheduled task's connections are wired to the cluster's EC2 capacity security groups " +
|
|
276
|
+
"(EcsCluster.getEc2CapacityConnectable), which an awsvpc task's own ENI does not wear, so every " +
|
|
277
|
+
"connection grant would be unreachable at runtime with nothing failing at synth. Remove networkMode " +
|
|
278
|
+
"to run on the instance ENI, or set NetworkMode.HOST. Govern the task's reachability with the " +
|
|
279
|
+
"cluster-level securityGroup on EcsClusterConfig.");
|
|
280
|
+
}
|
|
281
|
+
if (entry.logGroup !== undefined && entry.logRetention !== undefined) {
|
|
282
|
+
throw new Error(`Scheduled task '${entry.name}': "logGroup" and "logRetention" are mutually exclusive on ` +
|
|
283
|
+
"EcsScheduledTaskConfig — the supplied logGroup owns its own retention, so logRetention " +
|
|
284
|
+
`(${entry.logRetention}) would be silently discarded. Supply one or the other, not both.`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
272
287
|
}
|
|
273
288
|
const DEFAULT_MIGRATE_CONTAINER_NAME = "migrate";
|
|
274
289
|
/**
|
|
@@ -976,6 +991,7 @@ export class EcsCompute extends Construct {
|
|
|
976
991
|
applicationId: props.applicationId ??
|
|
977
992
|
(App.hasInstance() ? App.getInstance().getName() : undefined),
|
|
978
993
|
...(props.taskStopWatchdog === false && { taskStopWatchdog: false }),
|
|
994
|
+
...(props.deployableServices === false && { deployableServices: false }),
|
|
979
995
|
...(props.containerInsights !== undefined && {
|
|
980
996
|
containerInsights: props.containerInsights
|
|
981
997
|
}),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Repository } from "aws-cdk-lib/aws-ecr";
|
|
2
2
|
import { type RepositoryImage, type ContainerImage, type PortMapping, type Secret as EcsSecret, type NetworkMode } from "aws-cdk-lib/aws-ecs";
|
|
3
|
-
import { type IPeer, type ISecurityGroup, type IVpc, type Port
|
|
3
|
+
import { type IPeer, type ISecurityGroup, type IVpc, type Port } from "aws-cdk-lib/aws-ec2";
|
|
4
4
|
import { type ILogGroup, type RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
5
5
|
import { type PolicyDocument, type IManagedPolicy, type PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
6
6
|
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
@@ -554,7 +554,8 @@ export interface EcsScheduledTaskConfig {
|
|
|
554
554
|
* Connections to data resources — the same connector interface services
|
|
555
555
|
* use. Produces IAM grants on the task role, security-group wiring (via the
|
|
556
556
|
* cluster's EC2 capacity security group — scheduled tasks run on the
|
|
557
|
-
* cluster's instances
|
|
557
|
+
* cluster's instances, which is why `networkMode: AWS_VPC` is refused
|
|
558
|
+
* here), and schema-version gate env injection when a
|
|
558
559
|
* connected database declares `migrations:`. Scheduled tasks never
|
|
559
560
|
* materialise a `fjall-schema-gate` container; only the env is injected —
|
|
560
561
|
* the task's own entrypoint decides whether to gate on it.
|
|
@@ -576,8 +577,18 @@ export interface EcsScheduledTaskConfig {
|
|
|
576
577
|
* removal default (D17).
|
|
577
578
|
*/
|
|
578
579
|
logRetention?: RetentionDays;
|
|
579
|
-
|
|
580
|
-
|
|
580
|
+
/**
|
|
581
|
+
* Task network mode. Omit for the `Ec2TaskDefinition` default (BRIDGE, as
|
|
582
|
+
* measured on aws-cdk-lib 2.265.0 — the caret range can move it); `HOST` is
|
|
583
|
+
* permitted.
|
|
584
|
+
*
|
|
585
|
+
* `AWS_VPC` is REFUSED at synth. A scheduled task's `connections` are wired
|
|
586
|
+
* onto the cluster's EC2 capacity security groups
|
|
587
|
+
* (`EcsCluster.getEc2CapacityConnectable`), which an awsvpc task's own ENI
|
|
588
|
+
* does not wear — every grant would land and every network path would not,
|
|
589
|
+
* with nothing failing at synth. Govern reachability with the cluster-level
|
|
590
|
+
* `securityGroup` on `EcsClusterConfig` instead.
|
|
591
|
+
*/
|
|
581
592
|
networkMode?: NetworkMode;
|
|
582
593
|
/** Host-bind volumes mounted into the task's container. */
|
|
583
594
|
volumes?: ContainerVolume[];
|
|
@@ -641,6 +652,10 @@ export interface EcsClusterConfig {
|
|
|
641
652
|
* Scheduled tasks materialised into `Ec2TaskDefinition`s and registered via
|
|
642
653
|
* `app.addSchedule(...)` against the existing `EcsScheduleTarget` shape
|
|
643
654
|
* ({ ecs, serviceName, taskCount }). See AC34.
|
|
655
|
+
*
|
|
656
|
+
* These run on the cluster's own EC2 instances and share their network
|
|
657
|
+
* interface, so `networkMode: AWS_VPC` is refused per entry — see
|
|
658
|
+
* {@link EcsScheduledTaskConfig.networkMode}.
|
|
644
659
|
*/
|
|
645
660
|
scheduledTasks?: EcsScheduledTaskConfig[];
|
|
646
661
|
}
|
|
@@ -1081,6 +1096,17 @@ export interface EcsComputeProps {
|
|
|
1081
1096
|
* opt-out for cost-sensitive clusters.
|
|
1082
1097
|
*/
|
|
1083
1098
|
taskStopWatchdog?: false;
|
|
1099
|
+
/**
|
|
1100
|
+
* Deployable-service exports (default on): one `DeployableService` output
|
|
1101
|
+
* per service plus the cluster's `DeployableCluster` and `ClusterArn`
|
|
1102
|
+
* outputs, named by `@fjall/util`'s `stackExportNames` builders. The
|
|
1103
|
+
* deploy engine's `EcsServiceResolver` reads the app's Compute stack
|
|
1104
|
+
* outputs to find the ECS services that `fjall services`, `fjall rollout`,
|
|
1105
|
+
* `fjall restart`, a code-only deploy and a rollback act on. `false` opts
|
|
1106
|
+
* out a cluster hosting an infrastructure service, so the resolver never
|
|
1107
|
+
* binds it as an app service.
|
|
1108
|
+
*/
|
|
1109
|
+
deployableServices?: false;
|
|
1084
1110
|
/**
|
|
1085
1111
|
* CloudWatch Container Insights (default off). Enabling it bills
|
|
1086
1112
|
* ~$0.30/metric-month of custom `ECS/ContainerInsights` metrics per cluster
|
|
@@ -99,9 +99,9 @@ export class Domain extends Construct {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
|
-
* Re-publish the domain's effective zone name under a predictable
|
|
103
|
-
* key
|
|
104
|
-
*
|
|
102
|
+
* Re-publish the domain's effective zone name under a predictable output
|
|
103
|
+
* key (mirrored byte-for-byte by the eject template) so a stack reader can
|
|
104
|
+
* name the zone without knowing the registrar discriminant. The `Zone` and
|
|
105
105
|
* `HostedZoneId` outputs on the inner `HostedZone` construct cover the
|
|
106
106
|
* Route53-backed paths; this output covers external-records mode where no
|
|
107
107
|
* hosted zone exists.
|
|
@@ -7,12 +7,15 @@ export interface ExternalRecordsPatternResult {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Composition for `registrar: "external-records"`. No Route53 hosted zone is
|
|
10
|
-
* created
|
|
11
|
-
* validation
|
|
12
|
-
* records
|
|
13
|
-
*
|
|
10
|
+
* created. Certificates use ACM DNS validation with no zone to write to, so
|
|
11
|
+
* their validation CNAMEs must be created at the external registrar — and
|
|
12
|
+
* those records do not exist at synth: ACM chooses them when CloudFormation
|
|
13
|
+
* creates the certificate, and `AWS::CertificateManager::Certificate`
|
|
14
|
+
* returns only its ARN. They are therefore never `ManualRecord` entries:
|
|
15
|
+
* they are read from ACM (`DescribeCertificate`) once the certificate is
|
|
16
|
+
* pending, while CloudFormation waits for the records to resolve.
|
|
14
17
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
18
|
+
* User-declared records become `ManualRecord` entries, each with a
|
|
19
|
+
* `CfnOutput` triple keyed by its position in `records`.
|
|
17
20
|
*/
|
|
18
21
|
export declare function composeExternalRecords(scope: Construct, props: ExternalRecordsProps): ExternalRecordsPatternResult;
|
|
@@ -6,58 +6,35 @@ import { resolveRecordFqdn } from "../../utils/domainTypes.js";
|
|
|
6
6
|
const MANUAL_RECORD_TTL_SECONDS = 300;
|
|
7
7
|
/**
|
|
8
8
|
* Composition for `registrar: "external-records"`. No Route53 hosted zone is
|
|
9
|
-
* created
|
|
10
|
-
* validation
|
|
11
|
-
* records
|
|
12
|
-
*
|
|
9
|
+
* created. Certificates use ACM DNS validation with no zone to write to, so
|
|
10
|
+
* their validation CNAMEs must be created at the external registrar — and
|
|
11
|
+
* those records do not exist at synth: ACM chooses them when CloudFormation
|
|
12
|
+
* creates the certificate, and `AWS::CertificateManager::Certificate`
|
|
13
|
+
* returns only its ARN. They are therefore never `ManualRecord` entries:
|
|
14
|
+
* they are read from ACM (`DescribeCertificate`) once the certificate is
|
|
15
|
+
* pending, while CloudFormation waits for the records to resolve.
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* User-declared records become `ManualRecord` entries, each with a
|
|
18
|
+
* `CfnOutput` triple keyed by its position in `records`.
|
|
16
19
|
*/
|
|
17
20
|
export function composeExternalRecords(scope, props) {
|
|
18
21
|
const safeZone = toPascalCase(getSafeZoneName(props.zoneName));
|
|
19
22
|
const certificates = new Map();
|
|
20
23
|
const manualRecords = [];
|
|
21
|
-
// 1. Certificates — ACM DNS validation emits CNAMEs the user must paste at
|
|
22
|
-
// their registrar. Capture the validation records as ManualRecord
|
|
23
|
-
// entries (cert-validation first per ordering invariant).
|
|
24
24
|
const normalisedCerts = (props.certificates ?? []).map(normaliseCertificate);
|
|
25
25
|
normalisedCerts.forEach((cert, index) => {
|
|
26
26
|
const safeCertName = toPascalCase(cert.domainName.split(".").join(""));
|
|
27
27
|
const acm = new AcmCertificate(scope, `${safeZone}${safeCertName}Cert${index}`, {
|
|
28
28
|
domainName: cert.domainName,
|
|
29
29
|
subjectAlternativeNames: cert.subjectAlternativeNames,
|
|
30
|
-
// No hosted zone to validate against — ACM emits tokens for the user
|
|
31
|
-
// to resolve via `describeStackResources` or deploy output.
|
|
32
30
|
validation: CertificateValidation.fromDns(),
|
|
33
31
|
transparencyLoggingEnabled: cert.transparencyLogging ?? true
|
|
34
32
|
});
|
|
35
33
|
certificates.set(cert.domainName, acm);
|
|
36
|
-
const certNames = [
|
|
37
|
-
cert.domainName,
|
|
38
|
-
...(cert.subjectAlternativeNames ?? [])
|
|
39
|
-
];
|
|
40
|
-
certNames.forEach((name, nameIndex) => {
|
|
41
|
-
// `domainValidationOption` returns a token CNAME name/value pair. Using
|
|
42
|
-
// `attrDomainValidationOptions` returns an array token; per-name lookup
|
|
43
|
-
// uses index. CDK exposes the first validation option via
|
|
44
|
-
// `certificate.certificateArn` only — we fall back to emitting a
|
|
45
|
-
// predictable CfnOutput pointing to the attribute for Phase 2.
|
|
46
|
-
const record = {
|
|
47
|
-
name: `_acme-challenge.${name}`,
|
|
48
|
-
type: "CNAME",
|
|
49
|
-
value: `<ACM-validation-token-for-${name}>`,
|
|
50
|
-
ttl: MANUAL_RECORD_TTL_SECONDS,
|
|
51
|
-
reason: "certificate-validation"
|
|
52
|
-
};
|
|
53
|
-
manualRecords.push(record);
|
|
54
|
-
emitManualRecordOutputs(scope, safeZone, manualRecords.length - 1, record, `CertValidation${safeCertName}${nameIndex}`);
|
|
55
|
-
});
|
|
56
34
|
});
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// entries.
|
|
35
|
+
// Typed targets resolve to token strings via `resolveTargetToDnsName`.
|
|
36
|
+
// Apex ALIAS was already rejected by `validateDomainProps`, so all records
|
|
37
|
+
// here are safe to emit as manual entries.
|
|
61
38
|
const records = props.records ?? [];
|
|
62
39
|
records.forEach((record, index) => {
|
|
63
40
|
const fqdn = resolveRecordFqdn(record.name, props.zoneName);
|
|
@@ -67,11 +44,10 @@ export function composeExternalRecords(scope, props) {
|
|
|
67
44
|
name: fqdn,
|
|
68
45
|
type: "CNAME",
|
|
69
46
|
value,
|
|
70
|
-
ttl: MANUAL_RECORD_TTL_SECONDS
|
|
71
|
-
reason: "user-declared"
|
|
47
|
+
ttl: MANUAL_RECORD_TTL_SECONDS
|
|
72
48
|
};
|
|
73
49
|
manualRecords.push(manual);
|
|
74
|
-
emitManualRecordOutputs(scope, safeZone,
|
|
50
|
+
emitManualRecordOutputs(scope, safeZone, index, manual);
|
|
75
51
|
return;
|
|
76
52
|
}
|
|
77
53
|
const rawValue = record.value;
|
|
@@ -80,11 +56,10 @@ export function composeExternalRecords(scope, props) {
|
|
|
80
56
|
name: fqdn,
|
|
81
57
|
type: narrowManualType(record.type),
|
|
82
58
|
value: joined,
|
|
83
|
-
ttl: record.ttl ?? MANUAL_RECORD_TTL_SECONDS
|
|
84
|
-
reason: "user-declared"
|
|
59
|
+
ttl: record.ttl ?? MANUAL_RECORD_TTL_SECONDS
|
|
85
60
|
};
|
|
86
61
|
manualRecords.push(manual);
|
|
87
|
-
emitManualRecordOutputs(scope, safeZone,
|
|
62
|
+
emitManualRecordOutputs(scope, safeZone, index, manual);
|
|
88
63
|
});
|
|
89
64
|
return { certificates, manualRecords };
|
|
90
65
|
}
|
|
@@ -115,17 +90,18 @@ function narrowManualType(type) {
|
|
|
115
90
|
return "TXT";
|
|
116
91
|
}
|
|
117
92
|
}
|
|
118
|
-
function emitManualRecordOutputs(scope, safeZone, index, record
|
|
119
|
-
|
|
93
|
+
function emitManualRecordOutputs(scope, safeZone, index, record) {
|
|
94
|
+
const idPrefix = `${safeZone}ManualRecordUserRecord${index}`;
|
|
95
|
+
new CfnOutput(scope, `${idPrefix}Name`, {
|
|
120
96
|
key: `${safeZone}ManualRecord${index}Name`,
|
|
121
97
|
value: record.name,
|
|
122
|
-
description:
|
|
98
|
+
description: "External-records manual DNS entry (user-declared)"
|
|
123
99
|
});
|
|
124
|
-
new CfnOutput(scope, `${
|
|
100
|
+
new CfnOutput(scope, `${idPrefix}Type`, {
|
|
125
101
|
key: `${safeZone}ManualRecord${index}Type`,
|
|
126
102
|
value: record.type
|
|
127
103
|
});
|
|
128
|
-
new CfnOutput(scope, `${
|
|
104
|
+
new CfnOutput(scope, `${idPrefix}Value`, {
|
|
129
105
|
key: `${safeZone}ManualRecord${index}Value`,
|
|
130
106
|
value: record.value
|
|
131
107
|
});
|
|
@@ -158,15 +158,18 @@ export interface ExternalRecordsProps extends DomainCommonProps {
|
|
|
158
158
|
}
|
|
159
159
|
export type DomainProps = Route53ApexProps | ExternalDelegatedProps | ExternalRecordsProps;
|
|
160
160
|
/**
|
|
161
|
-
*
|
|
162
|
-
* to
|
|
163
|
-
*
|
|
164
|
-
*
|
|
161
|
+
* A user-declared record that `registrar: "external-records"` mode reports
|
|
162
|
+
* for the user to create at their external registrar, one per `records`
|
|
163
|
+
* entry, in declaration order. For typed targets the `value` is a
|
|
164
|
+
* CloudFormation token (`Fn::ImportValue`) at synth time; the concrete host
|
|
165
|
+
* is the deployed stack's `ManualRecord<index>Value` output.
|
|
166
|
+
*
|
|
167
|
+
* Certificate-validation records are never manual records: ACM chooses them
|
|
168
|
+
* at deploy time, so no synth-time value exists (see `composeExternalRecords`).
|
|
165
169
|
*/
|
|
166
170
|
export interface ManualRecord {
|
|
167
171
|
readonly name: string;
|
|
168
172
|
readonly type: "A" | "AAAA" | "CNAME" | "TXT";
|
|
169
173
|
readonly value: string;
|
|
170
174
|
readonly ttl: number;
|
|
171
|
-
readonly reason: "certificate-validation" | "user-declared";
|
|
172
175
|
}
|
|
@@ -18,6 +18,7 @@ import { type ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
|
18
18
|
import { type Duration } from "aws-cdk-lib";
|
|
19
19
|
import { type Construct } from "constructs";
|
|
20
20
|
import type { HstsConfig } from "../../../resources/aws/cdn/responseHeadersPolicy.js";
|
|
21
|
+
import type { OpenNextBucketKey } from "@fjall/util/patterns";
|
|
21
22
|
import { type RelationalDatabase, type DynamoDBDatabase, type ProxyConfig, type ReadReplicaConfig, type CredentialsConfig, type EncryptionConfig, type AuroraEncryptionConfig, type AuroraWriterConfig, type AuroraReadersConfig, type DatabaseInsightsConfig } from "../database.js";
|
|
22
23
|
import { type LambdaCompute } from "../compute.js";
|
|
23
24
|
import { type Storage } from "../storage.js";
|
|
@@ -210,22 +211,20 @@ export interface PatternStorageBucketConfig {
|
|
|
210
211
|
versioned?: boolean;
|
|
211
212
|
}
|
|
212
213
|
/**
|
|
213
|
-
* Full storage configuration for patterns
|
|
214
|
-
*
|
|
214
|
+
* Full storage configuration for patterns: one optional entry per bucket
|
|
215
|
+
* the OpenNext construct builds — `assets` (static files), `cache` (the ISR
|
|
216
|
+
* cache) and `media` (uploads) — keyed by `OPENNEXT_BUCKET_KEYS` in
|
|
217
|
+
* `@fjall/util/patterns`, the vocabulary the generator's config schema is
|
|
218
|
+
* pinned to as well.
|
|
215
219
|
*
|
|
216
220
|
* @example
|
|
217
221
|
* storage: {
|
|
218
222
|
* media: { versioned: true } // force on below the resilient tier
|
|
219
223
|
* }
|
|
220
224
|
*/
|
|
221
|
-
export
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
/** Cache bucket (ISR cache) */
|
|
225
|
-
cache?: PatternStorageBucketConfig;
|
|
226
|
-
/** Media bucket (uploads) */
|
|
227
|
-
media?: PatternStorageBucketConfig;
|
|
228
|
-
}
|
|
225
|
+
export type PayloadStorageConfig = {
|
|
226
|
+
[Bucket in OpenNextBucketKey]?: PatternStorageBucketConfig;
|
|
227
|
+
};
|
|
229
228
|
/**
|
|
230
229
|
* SQS queue configuration for pattern messaging.
|
|
231
230
|
*/
|
|
@@ -28,6 +28,16 @@ export interface IQueueProps {
|
|
|
28
28
|
maxReceiveCount?: number;
|
|
29
29
|
};
|
|
30
30
|
encryption?: "SSE_SQS" | "SSE_KMS" | "NONE";
|
|
31
|
+
/**
|
|
32
|
+
* ARN of a customer-managed KMS key to encrypt the queue (and its
|
|
33
|
+
* auto-created DLQ) with. Requires `encryption: "SSE_KMS"` — the resource
|
|
34
|
+
* layer refuses the pair rather than ignoring the key. Absent with
|
|
35
|
+
* `"SSE_KMS"`, the queue takes the stack's shared key.
|
|
36
|
+
*
|
|
37
|
+
* An ARN rather than an `IKey` because this is the plan vocabulary the
|
|
38
|
+
* scaffold and the codemod write, the same as `S3Props.kmsKeyArn`; a key
|
|
39
|
+
* you hold in `infrastructure.ts` passes as `key.keyArn`.
|
|
40
|
+
*/
|
|
31
41
|
kmsKeyArn?: string;
|
|
32
42
|
contentBasedDeduplication?: boolean;
|
|
33
43
|
fifoThroughputLimit?: "perQueue" | "perMessageGroupId";
|
|
@@ -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,
|