@fjall/components-infrastructure 36.0.0 → 38.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 +28 -0
- package/dist/lib/app.js +34 -2
- package/dist/lib/config/aws/disasterRecovery.d.ts +1 -2
- package/dist/lib/config/aws/disasterRecovery.js +52 -32
- package/dist/lib/patterns/aws/cdn.d.ts +19 -11
- package/dist/lib/patterns/aws/cdn.js +53 -25
- 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/payload.d.ts +5 -0
- package/dist/lib/patterns/aws/payload.js +23 -11
- 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 +17 -2
- package/dist/lib/resources/aws/backup/backupVault.js +37 -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/logging/accessLogBucket.d.ts +15 -8
- package/dist/lib/resources/aws/logging/accessLogBucket.js +16 -9
- package/dist/lib/resources/aws/messaging/eventBridgeQueueGrants.d.ts +14 -0
- package/dist/lib/resources/aws/messaging/eventBridgeQueueGrants.js +90 -0
- 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.js +4 -0
- package/dist/lib/resources/aws/messaging/subscription.d.ts +4 -3
- package/dist/lib/resources/aws/secrets/kms.d.ts +8 -0
- package/dist/lib/resources/aws/secrets/kms.js +12 -2
- package/dist/lib/resources/aws/secrets/stackEncryptionKey.js +6 -4
- package/dist/lib/utils/backupTierMapping.d.ts +11 -16
- package/dist/lib/utils/backupTierMapping.js +2 -15
- package/dist/lib/utils/cdkContext.d.ts +14 -0
- package/dist/lib/utils/cdkContext.js +38 -1
- package/dist/lib/utils/resourceNaming.d.ts +22 -0
- package/dist/lib/utils/resourceNaming.js +32 -0
- package/package.json +3 -3
|
@@ -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
|
*/
|
|
@@ -58,6 +58,11 @@ export declare class Payload extends Construct implements IPayload {
|
|
|
58
58
|
private buildBaseDatabaseProps;
|
|
59
59
|
private createAuroraDatabase;
|
|
60
60
|
private createInstanceDatabase;
|
|
61
|
+
/**
|
|
62
|
+
* Pinned to `OPENNEXT_BUCKET_KEYS` in both directions: a key the vocabulary
|
|
63
|
+
* gains fails here until a builder exists, and a builder for a key it does
|
|
64
|
+
* not carry is an excess property. Built in the vocabulary's order.
|
|
65
|
+
*/
|
|
61
66
|
private createStorageBuckets;
|
|
62
67
|
private createAssetsBucket;
|
|
63
68
|
private createCacheBucket;
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { Construct } from "constructs";
|
|
25
25
|
import { CfnOutput, Stack } from "aws-cdk-lib";
|
|
26
|
+
import { OPENNEXT_BUCKET_KEYS, patternBucketConstructId } from "@fjall/util/patterns";
|
|
26
27
|
import { Secret } from "../../resources/aws/secrets/index.js";
|
|
27
28
|
import { Code, Runtime, Architecture, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
|
|
28
29
|
import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
@@ -209,10 +210,25 @@ export class Payload extends Construct {
|
|
|
209
210
|
};
|
|
210
211
|
return this.app.addDatabase(DatabaseFactory.build(this.props.name, instanceProps));
|
|
211
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Pinned to `OPENNEXT_BUCKET_KEYS` in both directions: a key the vocabulary
|
|
215
|
+
* gains fails here until a builder exists, and a builder for a key it does
|
|
216
|
+
* not carry is an excess property. Built in the vocabulary's order.
|
|
217
|
+
*/
|
|
212
218
|
createStorageBuckets() {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
219
|
+
const builders = {
|
|
220
|
+
assets: () => {
|
|
221
|
+
this._assetsBucket = this.createAssetsBucket();
|
|
222
|
+
},
|
|
223
|
+
cache: () => {
|
|
224
|
+
this._cacheBucket = this.createCacheBucket();
|
|
225
|
+
},
|
|
226
|
+
media: () => {
|
|
227
|
+
this._mediaBucket = this.createMediaBucket();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
for (const bucket of OPENNEXT_BUCKET_KEYS)
|
|
231
|
+
builders[bucket]();
|
|
216
232
|
}
|
|
217
233
|
createAssetsBucket() {
|
|
218
234
|
const assetsConfig = this.props.storage?.assets ?? {};
|
|
@@ -230,7 +246,7 @@ export class Payload extends Construct {
|
|
|
230
246
|
}
|
|
231
247
|
}
|
|
232
248
|
};
|
|
233
|
-
return this.app.addStorage(StorageFactory.build(
|
|
249
|
+
return this.app.addStorage(StorageFactory.build(patternBucketConstructId(this.props.name, "assets"), assetsProps));
|
|
234
250
|
}
|
|
235
251
|
createCacheBucket() {
|
|
236
252
|
const cacheConfig = this.props.storage?.cache ?? {};
|
|
@@ -244,7 +260,7 @@ export class Payload extends Construct {
|
|
|
244
260
|
prune: false // ISR adds files at runtime
|
|
245
261
|
}
|
|
246
262
|
};
|
|
247
|
-
return this.app.addStorage(StorageFactory.build(
|
|
263
|
+
return this.app.addStorage(StorageFactory.build(patternBucketConstructId(this.props.name, "cache"), cacheProps));
|
|
248
264
|
}
|
|
249
265
|
createMediaBucket() {
|
|
250
266
|
const mediaConfig = this.props.storage?.media ?? {};
|
|
@@ -254,7 +270,7 @@ export class Payload extends Construct {
|
|
|
254
270
|
versioned: mediaConfig.versioned
|
|
255
271
|
})
|
|
256
272
|
};
|
|
257
|
-
return this.app.addStorage(StorageFactory.build(
|
|
273
|
+
return this.app.addStorage(StorageFactory.build(patternBucketConstructId(this.props.name, "media"), mediaProps));
|
|
258
274
|
}
|
|
259
275
|
createTagCache() {
|
|
260
276
|
const tagCacheProps = {
|
|
@@ -561,11 +577,7 @@ export class Payload extends Construct {
|
|
|
561
577
|
key: `${this.pascalName}PatternResources`,
|
|
562
578
|
value: JSON.stringify({
|
|
563
579
|
databases: [this.props.name, `${this.props.name}-tag-cache`],
|
|
564
|
-
storage:
|
|
565
|
-
`${this.props.name}-assets`,
|
|
566
|
-
`${this.props.name}-cache`,
|
|
567
|
-
`${this.props.name}-media`
|
|
568
|
-
],
|
|
580
|
+
storage: OPENNEXT_BUCKET_KEYS.map((bucket) => patternBucketConstructId(this.props.name, bucket)),
|
|
569
581
|
cdn: this.props.name
|
|
570
582
|
}),
|
|
571
583
|
description: `Related resource names for pattern ${this.props.name}`
|
|
@@ -64,9 +64,9 @@ export declare function resolveBucketTarget(appName: string, bucketName: string)
|
|
|
64
64
|
export declare function resolveCustomTarget(dnsName: string, hostedZoneId: string): IAliasRecordTarget;
|
|
65
65
|
/**
|
|
66
66
|
* Resolve a `FjallTarget` to its raw DNS-name string. The return value is a
|
|
67
|
-
* CloudFormation token for the ECS/CDN/bucket variants
|
|
68
|
-
* concrete host
|
|
69
|
-
* `externalRecordsPattern.ts` where no
|
|
70
|
-
* record emission needs a string value.
|
|
67
|
+
* CloudFormation token for the ECS/CDN/bucket variants, resolved by
|
|
68
|
+
* CloudFormation at deploy (the concrete host lands in the stack's manual
|
|
69
|
+
* record `Value` output). Used only by `externalRecordsPattern.ts` where no
|
|
70
|
+
* Route53 resource exists and manual record emission needs a string value.
|
|
71
71
|
*/
|
|
72
72
|
export declare function resolveTargetToDnsName(target: FjallTarget): string;
|
|
@@ -89,10 +89,10 @@ export function resolveCustomTarget(dnsName, hostedZoneId) {
|
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
91
|
* Resolve a `FjallTarget` to its raw DNS-name string. The return value is a
|
|
92
|
-
* CloudFormation token for the ECS/CDN/bucket variants
|
|
93
|
-
* concrete host
|
|
94
|
-
* `externalRecordsPattern.ts` where no
|
|
95
|
-
* record emission needs a string value.
|
|
92
|
+
* CloudFormation token for the ECS/CDN/bucket variants, resolved by
|
|
93
|
+
* CloudFormation at deploy (the concrete host lands in the stack's manual
|
|
94
|
+
* record `Value` output). Used only by `externalRecordsPattern.ts` where no
|
|
95
|
+
* Route53 resource exists and manual record emission needs a string value.
|
|
96
96
|
*/
|
|
97
97
|
export function resolveTargetToDnsName(target) {
|
|
98
98
|
switch (target.kind) {
|
|
@@ -2,13 +2,28 @@ import { Construct } from "constructs";
|
|
|
2
2
|
import { CfnOutput, RemovalPolicy } from "aws-cdk-lib";
|
|
3
3
|
import { BackupVault as Vault, type LockConfiguration } from "aws-cdk-lib/aws-backup";
|
|
4
4
|
import { type PolicyDocument } from "aws-cdk-lib/aws-iam";
|
|
5
|
-
import {
|
|
5
|
+
import { type IKey } from "aws-cdk-lib/aws-kms";
|
|
6
|
+
export declare function backupVaultKeyAliasName(vaultName: string): string;
|
|
6
7
|
export interface BackupVaultProps {
|
|
7
8
|
vaultName: string;
|
|
8
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The key the vault is encrypted with when it already exists outside the
|
|
11
|
+
* stack (a re-owned vault's live key). `EncryptionKeyArn` is create-only,
|
|
12
|
+
* so a re-owned vault must always name its live key or CloudFormation
|
|
13
|
+
* replaces it. The wrapper aliases the key and creates none. Omit to create
|
|
14
|
+
* a stack-managed key.
|
|
15
|
+
*/
|
|
16
|
+
encryptionKey?: IKey;
|
|
9
17
|
accessPolicy?: PolicyDocument;
|
|
10
18
|
lockConfiguration?: LockConfiguration;
|
|
11
19
|
removalPolicy?: RemovalPolicy;
|
|
20
|
+
/**
|
|
21
|
+
* Import pass: declare identity, not intent. Strips the mutable properties
|
|
22
|
+
* (`BackupVaultTags`, `AccessPolicy`) from the synthesised resource so the
|
|
23
|
+
* deploy's `--import-existing-resources` change set imports a live mirror;
|
|
24
|
+
* the converge deploy re-declares them.
|
|
25
|
+
*/
|
|
26
|
+
importIdentityOnly?: boolean;
|
|
12
27
|
}
|
|
13
28
|
export declare class BackupVault extends Construct {
|
|
14
29
|
readonly vault: Vault;
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { CfnOutput, RemovalPolicy } from "aws-cdk-lib";
|
|
3
|
-
import { BackupVault as Vault } from "aws-cdk-lib/aws-backup";
|
|
3
|
+
import { BackupVault as Vault, CfnBackupVault } from "aws-cdk-lib/aws-backup";
|
|
4
|
+
import { Alias } from "aws-cdk-lib/aws-kms";
|
|
4
5
|
import { CustomerManagedKey } from "../secrets/kms.js";
|
|
6
|
+
/**
|
|
7
|
+
* Vault properties CloudFormation applies as UPDATES, not identity. An import
|
|
8
|
+
* pass strips them so `--import-existing-resources` records a live mirror and
|
|
9
|
+
* the converge deploy that follows applies them as the diff (import records
|
|
10
|
+
* properties, it never applies them — a tag declared on the import pass is
|
|
11
|
+
* "already there" forever).
|
|
12
|
+
*/
|
|
13
|
+
const MUTABLE_VAULT_PROPERTIES = ["BackupVaultTags", "AccessPolicy"];
|
|
14
|
+
export function backupVaultKeyAliasName(vaultName) {
|
|
15
|
+
return `cmk/backupVault/${vaultName}`;
|
|
16
|
+
}
|
|
5
17
|
export class BackupVault extends Construct {
|
|
6
18
|
vault;
|
|
7
19
|
vaultArn;
|
|
@@ -11,21 +23,40 @@ export class BackupVault extends Construct {
|
|
|
11
23
|
// RETAIN default: a DESTROY-policy vault is silently deleted with the stack.
|
|
12
24
|
// Callers opt into DESTROY explicitly for ephemeral/dev vaults.
|
|
13
25
|
const removalPolicy = props.removalPolicy ?? RemovalPolicy.RETAIN;
|
|
14
|
-
const
|
|
15
|
-
|
|
26
|
+
const aliasName = backupVaultKeyAliasName(props.vaultName);
|
|
27
|
+
let encryptionKey;
|
|
28
|
+
if (props.encryptionKey !== undefined) {
|
|
29
|
+
encryptionKey = props.encryptionKey;
|
|
30
|
+
new Alias(this, `${props.vaultName}KeyAlias`, {
|
|
31
|
+
aliasName,
|
|
32
|
+
targetKey: encryptionKey
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
encryptionKey = new CustomerManagedKey(this, `${props.vaultName}Key`, {
|
|
16
37
|
description: `Encryption key for backup vault ${props.vaultName}`,
|
|
17
|
-
aliasName
|
|
38
|
+
aliasName,
|
|
18
39
|
// Recovery points in a retained vault outlive the stack.
|
|
19
40
|
protects: "outlives-stack",
|
|
20
41
|
removalPolicy
|
|
21
|
-
});
|
|
42
|
+
}).key;
|
|
43
|
+
}
|
|
22
44
|
this.vault = new Vault(this, `${props.vaultName}Vault`, {
|
|
23
45
|
backupVaultName: props.vaultName,
|
|
24
|
-
encryptionKey
|
|
46
|
+
encryptionKey,
|
|
25
47
|
accessPolicy: props.accessPolicy,
|
|
26
48
|
lockConfiguration: props.lockConfiguration,
|
|
27
49
|
removalPolicy
|
|
28
50
|
});
|
|
51
|
+
if (props.importIdentityOnly === true) {
|
|
52
|
+
const cfnVault = this.vault.node.defaultChild;
|
|
53
|
+
if (!(cfnVault instanceof CfnBackupVault)) {
|
|
54
|
+
throw new Error(`Backup vault "${props.vaultName}": expected an AWS::Backup::BackupVault default child to strip for import`);
|
|
55
|
+
}
|
|
56
|
+
for (const property of MUTABLE_VAULT_PROPERTIES) {
|
|
57
|
+
cfnVault.addPropertyDeletionOverride(property);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
29
60
|
this.vaultArn = new CfnOutput(this, `${props.vaultName}VaultArn`, {
|
|
30
61
|
key: `${props.vaultName}VaultArn`,
|
|
31
62
|
value: this.vault.backupVaultArn,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { type CloudFrontPriceClass } from "@fjall/util";
|
|
3
|
+
import { CloudFrontLogDelivery, type CloudFrontLogFormat } from "./cloudFrontLogDelivery.js";
|
|
3
4
|
import { Distribution, type ICachePolicy, type IResponseHeadersPolicy } from "aws-cdk-lib/aws-cloudfront";
|
|
4
5
|
import { type IBucket } from "aws-cdk-lib/aws-s3";
|
|
5
6
|
import { type IApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
@@ -59,25 +60,13 @@ export interface CloudFrontDistributionProps {
|
|
|
59
60
|
certificate?: ICertificate;
|
|
60
61
|
comment?: string;
|
|
61
62
|
/**
|
|
62
|
-
* Standard (
|
|
63
|
-
*
|
|
64
|
-
* distribution
|
|
65
|
-
*
|
|
66
|
-
*
|
|
63
|
+
* Standard logging (v2): access logs delivered through CloudWatch Logs to
|
|
64
|
+
* an S3 bucket in any region, including opt-in ones. Omit for no logging.
|
|
65
|
+
* The distribution's own `Logging` block (legacy logging) is never
|
|
66
|
+
* rendered — the legacy `enableLogging`/`logBucket`/`logFilePrefix` props
|
|
67
|
+
* are refused at synth since 38.0.0.
|
|
67
68
|
*/
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Delivery bucket for standard logs; without it CDK creates one in the
|
|
71
|
-
* distribution's own stack (and region). Its `env.region` is the delivery
|
|
72
|
-
* region the opt-in refusal on `enableLogging` checks, so an imported
|
|
73
|
-
* bucket must declare it: `Bucket.fromBucketAttributes(…, { region })`, or
|
|
74
|
-
* a `Bucket` in a stack pinned to its region. `fromBucketName` and
|
|
75
|
-
* `fromBucketArn` inherit this stack's region, and a token bucket region
|
|
76
|
-
* falls back to it.
|
|
77
|
-
*/
|
|
78
|
-
logBucket?: IBucket;
|
|
79
|
-
/** Key prefix for standard logs delivered to `logBucket`. */
|
|
80
|
-
logFilePrefix?: string;
|
|
69
|
+
logging?: CloudFrontLoggingProps;
|
|
81
70
|
priceClass?: CloudFrontPriceClass;
|
|
82
71
|
/** Adds a CloudFront Function to copy the viewer Host header into X-Forwarded-Host.
|
|
83
72
|
* Required when the origin is a Lambda Function URL or ALB behind CloudFront,
|
|
@@ -121,8 +110,43 @@ export interface CloudFrontDistributionProps {
|
|
|
121
110
|
*/
|
|
122
111
|
originRequestPolicy?: "all-viewer-except-host" | "none";
|
|
123
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Standard logging (v2) for a distribution. The delivery resources are
|
|
115
|
+
* created in `deliveryScope` — which MUST be a us-east-1 stack, the only
|
|
116
|
+
* region CloudWatch Logs accepts them in — and default to the distribution's
|
|
117
|
+
* own scope, which is right only when the distribution's stack is itself in
|
|
118
|
+
* us-east-1. The `Cdn` pattern resolves the scope through
|
|
119
|
+
* `App.getUsEast1CdnLoggingStack()`; direct callers in another region pass
|
|
120
|
+
* a us-east-1 stack themselves.
|
|
121
|
+
*/
|
|
122
|
+
export interface CloudFrontLoggingProps {
|
|
123
|
+
/** Bucket the logs land in — same account as the distribution. */
|
|
124
|
+
bucket: IBucket;
|
|
125
|
+
/** Key prefix (no leading or trailing slash); defaults to `cloudfront/<id>`. */
|
|
126
|
+
prefix?: string;
|
|
127
|
+
/** CloudWatch Logs output format; defaults to `json`. */
|
|
128
|
+
format?: CloudFrontLogFormat;
|
|
129
|
+
/** Scope for the delivery resources; a us-east-1 stack. */
|
|
130
|
+
deliveryScope?: Construct;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The props CloudFront standard (legacy) logging took before 38.0.0. They
|
|
134
|
+
* are not members of `CloudFrontDistributionProps` any more, so a
|
|
135
|
+
* TypeScript caller cannot pass them; a JavaScript caller, a spread from a
|
|
136
|
+
* saved config or a generator template still can, and the legacy `Logging`
|
|
137
|
+
* block would otherwise be silently dropped. Refused at synth by name.
|
|
138
|
+
*/
|
|
139
|
+
export declare const LEGACY_CLOUDFRONT_LOGGING_PROPS: readonly ["enableLogging", "logBucket", "logFilePrefix"];
|
|
140
|
+
/**
|
|
141
|
+
* `replacement` is the seam's v2 shape, named in the refusal: the resource
|
|
142
|
+
* requires the bucket and has no `false`, the pattern defaults the bucket
|
|
143
|
+
* and accepts `false`.
|
|
144
|
+
*/
|
|
145
|
+
export declare function assertNoLegacyLoggingProps(context: string, props: object, replacement: string): void;
|
|
124
146
|
export declare class CloudFrontDistribution extends Construct {
|
|
125
147
|
readonly id: string;
|
|
148
|
+
/** The v2 log delivery, when `logging` was given. */
|
|
149
|
+
readonly logDelivery?: CloudFrontLogDelivery;
|
|
126
150
|
private distribution;
|
|
127
151
|
private readonly origins;
|
|
128
152
|
/** Shared S3 access identities, keyed by the owning stack (see
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
|
-
import { CfnOutput, Names, Stack
|
|
2
|
+
import { CfnOutput, Names, Stack } from "aws-cdk-lib";
|
|
3
3
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
4
4
|
import { registerCdnAliasTarget } from "../../../utils/cdnAliasTargetRegistry.js";
|
|
5
|
-
import { cdnDomainExportName
|
|
5
|
+
import { cdnDomainExportName } from "@fjall/util";
|
|
6
|
+
import { CloudFrontLogDelivery, DEFAULT_CLOUDFRONT_LOG_FORMAT } from "./cloudFrontLogDelivery.js";
|
|
6
7
|
import { Distribution, PriceClass, ViewerProtocolPolicy, CachePolicy, OriginRequestPolicy, AllowedMethods, OriginProtocolPolicy, Function as CloudFrontFunction, FunctionCode, FunctionRuntime, FunctionEventType, OriginAccessIdentity, S3OriginAccessControl } from "aws-cdk-lib/aws-cloudfront";
|
|
7
8
|
import { S3BucketOrigin, LoadBalancerV2Origin, HttpOrigin } from "aws-cdk-lib/aws-cloudfront-origins";
|
|
8
9
|
/**
|
|
@@ -43,56 +44,38 @@ const URI_REWRITE_FRAGMENTS = {
|
|
|
43
44
|
" if (seg.indexOf('.') === -1) { request.uri = uri + '/index.html'; } } }"
|
|
44
45
|
};
|
|
45
46
|
/**
|
|
46
|
-
* CloudFront standard (legacy) logging
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* distribution crosses, whether built directly or through the `Cdn`
|
|
52
|
-
* pattern — and names both cures.
|
|
53
|
-
*
|
|
54
|
-
* Logging is on exactly when `renderLogging` emits a Logging block:
|
|
55
|
-
* `enableLogging: true`, or a `logBucket` without an explicit
|
|
56
|
-
* `enableLogging: false` (that pair is CDK's own contradiction error and is
|
|
57
|
-
* left to it). The delivery region is the bucket's `env.region` when it is
|
|
58
|
-
* concrete, else the distribution stack's — the bucket CDK would create.
|
|
59
|
-
* A token delivery region (an unpinned stack, and no region on the bucket)
|
|
60
|
-
* cannot be checked and passes. A concrete region `AWS_REGIONS_METADATA`
|
|
61
|
-
* does not list is refused too — every region launched since 2019 is opt-in,
|
|
62
|
-
* so an unlisted one cannot be assumed default-enabled — naming the metadata
|
|
63
|
-
* as the cure. The guard fires whenever the delivery region is concrete and
|
|
64
|
-
* known to the metadata.
|
|
47
|
+
* The props CloudFront standard (legacy) logging took before 38.0.0. They
|
|
48
|
+
* are not members of `CloudFrontDistributionProps` any more, so a
|
|
49
|
+
* TypeScript caller cannot pass them; a JavaScript caller, a spread from a
|
|
50
|
+
* saved config or a generator template still can, and the legacy `Logging`
|
|
51
|
+
* block would otherwise be silently dropped. Refused at synth by name.
|
|
65
52
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
if (!OPT_IN_REGION_CODES.has(region))
|
|
53
|
+
export const LEGACY_CLOUDFRONT_LOGGING_PROPS = [
|
|
54
|
+
"enableLogging",
|
|
55
|
+
"logBucket",
|
|
56
|
+
"logFilePrefix"
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* `replacement` is the seam's v2 shape, named in the refusal: the resource
|
|
60
|
+
* requires the bucket and has no `false`, the pattern defaults the bucket
|
|
61
|
+
* and accepts `false`.
|
|
62
|
+
*/
|
|
63
|
+
export function assertNoLegacyLoggingProps(context, props, replacement) {
|
|
64
|
+
const legacy = new Set(LEGACY_CLOUDFRONT_LOGGING_PROPS);
|
|
65
|
+
const present = Object.entries(props)
|
|
66
|
+
.filter(([key, value]) => legacy.has(key) && value !== undefined)
|
|
67
|
+
.map(([key]) => key);
|
|
68
|
+
if (present.length === 0)
|
|
84
69
|
return;
|
|
85
|
-
throw new Error(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"
|
|
89
|
-
"whose region CDK knows to be default-enabled: " +
|
|
90
|
-
"Bucket.fromBucketAttributes(…, { region }), or a bucket in a stack " +
|
|
91
|
-
"pinned to that region (fromBucketName and fromBucketArn inherit this " +
|
|
92
|
-
"stack's region).");
|
|
70
|
+
throw new Error(`${context}: ${present.join(", ")} ${present.length === 1 ? "is" : "are"} ` +
|
|
71
|
+
"not supported since 38.0.0 — CloudFront standard (legacy) logging was " +
|
|
72
|
+
`replaced by CloudWatch-vended standard logging (v2). Use ${replacement} ` +
|
|
73
|
+
"instead; see docs/release-notes/38.0.0.md for the migration.");
|
|
93
74
|
}
|
|
94
75
|
export class CloudFrontDistribution extends Construct {
|
|
95
76
|
id;
|
|
77
|
+
/** The v2 log delivery, when `logging` was given. */
|
|
78
|
+
logDelivery;
|
|
96
79
|
distribution;
|
|
97
80
|
origins = new Map();
|
|
98
81
|
/** Shared S3 access identities, keyed by the owning stack (see
|
|
@@ -102,7 +85,7 @@ export class CloudFrontDistribution extends Construct {
|
|
|
102
85
|
constructor(scope, id, props) {
|
|
103
86
|
super(scope, id);
|
|
104
87
|
this.id = id;
|
|
105
|
-
|
|
88
|
+
assertNoLegacyLoggingProps(`CloudFront distribution '${id}'`, props, "logging: { bucket, prefix?, format?, deliveryScope? } (or no logging prop at all)");
|
|
106
89
|
// Sanitise id for CloudFormation output keys (must be alphanumeric)
|
|
107
90
|
const outputName = toPascalCase(id);
|
|
108
91
|
const defaultOrigin = this.createOrigin(props.defaultOrigin);
|
|
@@ -158,11 +141,16 @@ export class CloudFrontDistribution extends Construct {
|
|
|
158
141
|
domainNames: props.domainNames,
|
|
159
142
|
certificate: props.certificate,
|
|
160
143
|
comment: props.comment,
|
|
161
|
-
enableLogging: props.enableLogging,
|
|
162
|
-
logBucket: props.logBucket,
|
|
163
|
-
logFilePrefix: props.logFilePrefix,
|
|
164
144
|
priceClass: this.resolvePriceClass(props.priceClass)
|
|
165
145
|
});
|
|
146
|
+
if (props.logging !== undefined) {
|
|
147
|
+
this.logDelivery = new CloudFrontLogDelivery(props.logging.deliveryScope ?? this, `${id}LogDelivery`, {
|
|
148
|
+
distribution: this.distribution,
|
|
149
|
+
bucket: props.logging.bucket,
|
|
150
|
+
prefix: props.logging.prefix ?? `cloudfront/${id}`,
|
|
151
|
+
format: props.logging.format ?? DEFAULT_CLOUDFRONT_LOG_FORMAT
|
|
152
|
+
});
|
|
153
|
+
}
|
|
166
154
|
// The CDN-qualified export is unconditional and always unique, so an app
|
|
167
155
|
// fronting several distributions stays deployable. The unqualified
|
|
168
156
|
// app-level export is emitted once from `App.synth()`, and only when the
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type IDistribution } from "aws-cdk-lib/aws-cloudfront";
|
|
2
|
+
import { CfnDelivery, CfnDeliveryDestination, CfnDeliverySource } from "aws-cdk-lib/aws-logs";
|
|
3
|
+
import { type IBucket } from "aws-cdk-lib/aws-s3";
|
|
4
|
+
import { Construct } from "constructs";
|
|
5
|
+
/**
|
|
6
|
+
* Output formats CloudWatch Logs writes a CloudFront access-log delivery in.
|
|
7
|
+
* `json` is the default; `parquet` adds CloudWatch's conversion charge on top
|
|
8
|
+
* of delivery. Fixed at creation — a change replaces the destination.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CLOUDFRONT_LOG_FORMATS: readonly ["json", "parquet", "plain", "w3c", "raw"];
|
|
11
|
+
export type CloudFrontLogFormat = (typeof CLOUDFRONT_LOG_FORMATS)[number];
|
|
12
|
+
export declare const DEFAULT_CLOUDFRONT_LOG_FORMAT: CloudFrontLogFormat;
|
|
13
|
+
/**
|
|
14
|
+
* CloudWatch Logs mints every CloudFront delivery source, destination and
|
|
15
|
+
* delivery in us-east-1 whatever region the bucket is in — the delivery ARNs
|
|
16
|
+
* read `arn:aws:logs:us-east-1:…` and the API refuses other regions.
|
|
17
|
+
*/
|
|
18
|
+
export declare const CLOUDFRONT_LOG_DELIVERY_REGION = "us-east-1";
|
|
19
|
+
/** The service principal that writes vended logs into the bucket. */
|
|
20
|
+
export declare const CLOUDFRONT_LOG_DELIVERY_PRINCIPAL = "delivery.logs.amazonaws.com";
|
|
21
|
+
/** Objects land at `<prefix>/<yyyy>/<MM>/<dd>/<HH>/…`. Changeable in place. */
|
|
22
|
+
export declare const CLOUDFRONT_LOG_S3_SUFFIX_PATH = "{yyyy}/{MM}/{dd}/{HH}";
|
|
23
|
+
/**
|
|
24
|
+
* Every delivery source and destination is named under this prefix —
|
|
25
|
+
* `fjall-cf-<distributionId>` for the source, `fjall-cf-<distributionId>-<digest>`
|
|
26
|
+
* for the destination. The deploy-test residue scan attributes delivery
|
|
27
|
+
* resources by the prefix and the digest width and restates both; a change
|
|
28
|
+
* here is a change to what the scan can see.
|
|
29
|
+
*/
|
|
30
|
+
export declare const CLOUDFRONT_LOG_DELIVERY_NAME_PREFIX = "fjall-cf-";
|
|
31
|
+
/** Hex characters of the sha256 the destination name carries. */
|
|
32
|
+
export declare const CLOUDFRONT_LOG_DELIVERY_DIGEST_LENGTH = 8;
|
|
33
|
+
export interface CloudFrontLogDeliveryProps {
|
|
34
|
+
/**
|
|
35
|
+
* The distribution whose access logs are delivered. CloudWatch Logs allows
|
|
36
|
+
* one delivery source per distribution account-wide, so the source is
|
|
37
|
+
* named by the distribution id and nothing else.
|
|
38
|
+
*/
|
|
39
|
+
readonly distribution: IDistribution;
|
|
40
|
+
/** The bucket the logs land in; same account as the distribution. */
|
|
41
|
+
readonly bucket: IBucket;
|
|
42
|
+
/** Key prefix the objects land under — see `normaliseCloudFrontLogPrefix`. */
|
|
43
|
+
readonly prefix: string;
|
|
44
|
+
readonly format: CloudFrontLogFormat;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The prefix is part of the destination ARN (`arn:aws:s3:::<bucket>/<prefix>`),
|
|
48
|
+
* a create-only property, and it keys the destination's name, so it must be
|
|
49
|
+
* a literal at synth. Leading and trailing slashes are dropped (CloudWatch
|
|
50
|
+
* joins the suffix path with its own separator); an empty prefix is refused
|
|
51
|
+
* rather than silently selecting the bucket-root `AWSLogs/…` layout.
|
|
52
|
+
*/
|
|
53
|
+
export declare function normaliseCloudFrontLogPrefix(context: string, prefix: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* CloudFront standard logging (v2): the CloudWatch Logs delivery source
|
|
56
|
+
* (the distribution), delivery destination (the bucket and prefix) and the
|
|
57
|
+
* delivery joining them, plus the bucket-policy statement the delivery
|
|
58
|
+
* principal writes through. The names — `fjall-cf-<distributionId>` and
|
|
59
|
+
* `fjall-cf-<distributionId>-<digest>` — carry no construct path, but
|
|
60
|
+
* CloudFormation replaces on the LOGICAL id and creates the replacement
|
|
61
|
+
* before deleting the original, and CloudWatch refuses a second source for
|
|
62
|
+
* the same distribution. So the three logical ids are load-bearing once a
|
|
63
|
+
* delivery is live: a re-layout of this construct or its owner strands the
|
|
64
|
+
* update in rollback, and is a breaking change with a migration (delete
|
|
65
|
+
* the live delivery first) rather than a refactor. The delivery test pins
|
|
66
|
+
* the ids in both layouts.
|
|
67
|
+
*
|
|
68
|
+
* The scope MUST be a us-east-1 stack (asserted at synth). When the
|
|
69
|
+
* distribution and bucket live in another region's stack, their ARNs reach
|
|
70
|
+
* this stack through CDK cross-region references; both are stable under an
|
|
71
|
+
* unchanged logical id, which the export writer requires.
|
|
72
|
+
*/
|
|
73
|
+
export declare class CloudFrontLogDelivery extends Construct {
|
|
74
|
+
readonly sourceName: string;
|
|
75
|
+
readonly destinationName: string;
|
|
76
|
+
readonly source: CfnDeliverySource;
|
|
77
|
+
readonly destination: CfnDeliveryDestination;
|
|
78
|
+
readonly delivery: CfnDelivery;
|
|
79
|
+
constructor(scope: Construct, id: string, props: CloudFrontLogDeliveryProps);
|
|
80
|
+
/**
|
|
81
|
+
* The bucket-policy statement the delivery principal writes through, and
|
|
82
|
+
* the key-policy statement when the bucket is KMS-encrypted. Both carry the
|
|
83
|
+
* confused-deputy conditions; the bucket statement additionally requires
|
|
84
|
+
* `bucket-owner-full-control`, the one ACL a BucketOwnerEnforced bucket
|
|
85
|
+
* accepts. Imported buckets and keys are not managed here — a synth
|
|
86
|
+
* warning names the statement the owner must add, because a missing grant
|
|
87
|
+
* fails delivery silently rather than at deploy.
|
|
88
|
+
*/
|
|
89
|
+
private grantDeliveryWrite;
|
|
90
|
+
}
|