@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Annotations, Stack, Token } from "aws-cdk-lib";
|
|
3
|
+
import { Effect, PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
4
|
+
import { CfnDelivery, CfnDeliveryDestination, CfnDeliverySource } from "aws-cdk-lib/aws-logs";
|
|
5
|
+
import { Construct } from "constructs";
|
|
6
|
+
/**
|
|
7
|
+
* Output formats CloudWatch Logs writes a CloudFront access-log delivery in.
|
|
8
|
+
* `json` is the default; `parquet` adds CloudWatch's conversion charge on top
|
|
9
|
+
* of delivery. Fixed at creation — a change replaces the destination.
|
|
10
|
+
*/
|
|
11
|
+
export const CLOUDFRONT_LOG_FORMATS = [
|
|
12
|
+
"json",
|
|
13
|
+
"parquet",
|
|
14
|
+
"plain",
|
|
15
|
+
"w3c",
|
|
16
|
+
"raw"
|
|
17
|
+
];
|
|
18
|
+
export const DEFAULT_CLOUDFRONT_LOG_FORMAT = "json";
|
|
19
|
+
/**
|
|
20
|
+
* CloudWatch Logs mints every CloudFront delivery source, destination and
|
|
21
|
+
* delivery in us-east-1 whatever region the bucket is in — the delivery ARNs
|
|
22
|
+
* read `arn:aws:logs:us-east-1:…` and the API refuses other regions.
|
|
23
|
+
*/
|
|
24
|
+
export const CLOUDFRONT_LOG_DELIVERY_REGION = "us-east-1";
|
|
25
|
+
/** The service principal that writes vended logs into the bucket. */
|
|
26
|
+
export const CLOUDFRONT_LOG_DELIVERY_PRINCIPAL = "delivery.logs.amazonaws.com";
|
|
27
|
+
/** Objects land at `<prefix>/<yyyy>/<MM>/<dd>/<HH>/…`. Changeable in place. */
|
|
28
|
+
export const CLOUDFRONT_LOG_S3_SUFFIX_PATH = "{yyyy}/{MM}/{dd}/{HH}";
|
|
29
|
+
/**
|
|
30
|
+
* Every delivery source and destination is named under this prefix —
|
|
31
|
+
* `fjall-cf-<distributionId>` for the source, `fjall-cf-<distributionId>-<digest>`
|
|
32
|
+
* for the destination. The deploy-test residue scan attributes delivery
|
|
33
|
+
* resources by the prefix and the digest width and restates both; a change
|
|
34
|
+
* here is a change to what the scan can see.
|
|
35
|
+
*/
|
|
36
|
+
export const CLOUDFRONT_LOG_DELIVERY_NAME_PREFIX = "fjall-cf-";
|
|
37
|
+
/** Hex characters of the sha256 the destination name carries. */
|
|
38
|
+
export const CLOUDFRONT_LOG_DELIVERY_DIGEST_LENGTH = 8;
|
|
39
|
+
/**
|
|
40
|
+
* The prefix is part of the destination ARN (`arn:aws:s3:::<bucket>/<prefix>`),
|
|
41
|
+
* a create-only property, and it keys the destination's name, so it must be
|
|
42
|
+
* a literal at synth. Leading and trailing slashes are dropped (CloudWatch
|
|
43
|
+
* joins the suffix path with its own separator); an empty prefix is refused
|
|
44
|
+
* rather than silently selecting the bucket-root `AWSLogs/…` layout.
|
|
45
|
+
*/
|
|
46
|
+
export function normaliseCloudFrontLogPrefix(context, prefix) {
|
|
47
|
+
if (Token.isUnresolved(prefix)) {
|
|
48
|
+
throw new Error(`${context}: the log prefix must be a literal string at synth — it ` +
|
|
49
|
+
"keys the delivery destination's name — but a token was given.");
|
|
50
|
+
}
|
|
51
|
+
const normalised = prefix.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
52
|
+
if (normalised === "") {
|
|
53
|
+
throw new Error(`${context}: the log prefix is empty after dropping slashes. Give a ` +
|
|
54
|
+
"key prefix such as 'cloudfront/<id>', or omit it for the default.");
|
|
55
|
+
}
|
|
56
|
+
return normalised;
|
|
57
|
+
}
|
|
58
|
+
function assertDeliveryRegion(context, stack) {
|
|
59
|
+
const region = stack.region;
|
|
60
|
+
if (Token.isUnresolved(region)) {
|
|
61
|
+
throw new Error(`${context}: the delivery stack's region is unresolved at synth (a ` +
|
|
62
|
+
`region token). CloudWatch Logs accepts these resources in ` +
|
|
63
|
+
`${CLOUDFRONT_LOG_DELIVERY_REGION} only, so the stack must be ` +
|
|
64
|
+
"env-pinned — deploy through the Fjall CLI, or export " +
|
|
65
|
+
"CDK_DEFAULT_ACCOUNT and CDK_DEFAULT_REGION before synthesising.");
|
|
66
|
+
}
|
|
67
|
+
if (region !== CLOUDFRONT_LOG_DELIVERY_REGION) {
|
|
68
|
+
throw new Error(`${context}: CloudWatch Logs accepts CloudFront delivery resources in ` +
|
|
69
|
+
`${CLOUDFRONT_LOG_DELIVERY_REGION} only, but the scope's stack ` +
|
|
70
|
+
`'${stack.stackName}' is in ${region}. Create the delivery in the ` +
|
|
71
|
+
"app's us-east-1 logging stack (App.getUsEast1CdnLoggingStack()) — " +
|
|
72
|
+
"the Cdn pattern places it there itself.");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Everything create-only on the destination — the bucket, the prefix and
|
|
77
|
+
* the format — folds into its name, so a change replaces the destination
|
|
78
|
+
* under a NEW name: CloudFormation creates the replacement before deleting
|
|
79
|
+
* the original, and an identical name would collide. An imported bucket
|
|
80
|
+
* contributes its literal name; a bucket built here contributes its
|
|
81
|
+
* construct path (its physical name is a token), which is stable under an
|
|
82
|
+
* unchanged logical id — the one residual is a same-id bucket replacement,
|
|
83
|
+
* which keeps the name. Nothing tokenised is hashed: token spellings vary
|
|
84
|
+
* between synths.
|
|
85
|
+
*/
|
|
86
|
+
function destinationDigest(bucket, prefix, format) {
|
|
87
|
+
const bucketKey = Token.isUnresolved(bucket.bucketName)
|
|
88
|
+
? bucket.node.path
|
|
89
|
+
: bucket.bucketName;
|
|
90
|
+
return createHash("sha256")
|
|
91
|
+
.update(`${bucketKey}|${prefix}|${format}`)
|
|
92
|
+
.digest("hex")
|
|
93
|
+
.slice(0, CLOUDFRONT_LOG_DELIVERY_DIGEST_LENGTH);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* CloudFront standard logging (v2): the CloudWatch Logs delivery source
|
|
97
|
+
* (the distribution), delivery destination (the bucket and prefix) and the
|
|
98
|
+
* delivery joining them, plus the bucket-policy statement the delivery
|
|
99
|
+
* principal writes through. The names — `fjall-cf-<distributionId>` and
|
|
100
|
+
* `fjall-cf-<distributionId>-<digest>` — carry no construct path, but
|
|
101
|
+
* CloudFormation replaces on the LOGICAL id and creates the replacement
|
|
102
|
+
* before deleting the original, and CloudWatch refuses a second source for
|
|
103
|
+
* the same distribution. So the three logical ids are load-bearing once a
|
|
104
|
+
* delivery is live: a re-layout of this construct or its owner strands the
|
|
105
|
+
* update in rollback, and is a breaking change with a migration (delete
|
|
106
|
+
* the live delivery first) rather than a refactor. The delivery test pins
|
|
107
|
+
* the ids in both layouts.
|
|
108
|
+
*
|
|
109
|
+
* The scope MUST be a us-east-1 stack (asserted at synth). When the
|
|
110
|
+
* distribution and bucket live in another region's stack, their ARNs reach
|
|
111
|
+
* this stack through CDK cross-region references; both are stable under an
|
|
112
|
+
* unchanged logical id, which the export writer requires.
|
|
113
|
+
*/
|
|
114
|
+
export class CloudFrontLogDelivery extends Construct {
|
|
115
|
+
sourceName;
|
|
116
|
+
destinationName;
|
|
117
|
+
source;
|
|
118
|
+
destination;
|
|
119
|
+
delivery;
|
|
120
|
+
constructor(scope, id, props) {
|
|
121
|
+
super(scope, id);
|
|
122
|
+
const context = `CloudFront log delivery '${id}'`;
|
|
123
|
+
const stack = Stack.of(this);
|
|
124
|
+
assertDeliveryRegion(context, stack);
|
|
125
|
+
const { distribution, bucket, format } = props;
|
|
126
|
+
const prefix = normaliseCloudFrontLogPrefix(context, props.prefix);
|
|
127
|
+
this.sourceName = `${CLOUDFRONT_LOG_DELIVERY_NAME_PREFIX}${distribution.distributionId}`;
|
|
128
|
+
this.destinationName = `${this.sourceName}-${destinationDigest(bucket, prefix, format)}`;
|
|
129
|
+
this.source = new CfnDeliverySource(this, "Source", {
|
|
130
|
+
name: this.sourceName,
|
|
131
|
+
resourceArn: distribution.distributionArn,
|
|
132
|
+
logType: "ACCESS_LOGS"
|
|
133
|
+
});
|
|
134
|
+
this.destination = new CfnDeliveryDestination(this, "Destination", {
|
|
135
|
+
name: this.destinationName,
|
|
136
|
+
deliveryDestinationType: "S3",
|
|
137
|
+
destinationResourceArn: `${bucket.bucketArn}/${prefix}`,
|
|
138
|
+
outputFormat: format
|
|
139
|
+
});
|
|
140
|
+
this.delivery = new CfnDelivery(this, "Delivery", {
|
|
141
|
+
deliverySourceName: this.sourceName,
|
|
142
|
+
deliveryDestinationArn: this.destination.attrArn,
|
|
143
|
+
s3SuffixPath: CLOUDFRONT_LOG_S3_SUFFIX_PATH,
|
|
144
|
+
s3EnableHiveCompatiblePath: false
|
|
145
|
+
});
|
|
146
|
+
// `deliverySourceName` is a plain string: CloudFormation sees no edge to
|
|
147
|
+
// the source and would happily create the delivery first.
|
|
148
|
+
this.delivery.addDependency(this.source);
|
|
149
|
+
this.grantDeliveryWrite(context, bucket, prefix, stack);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The bucket-policy statement the delivery principal writes through, and
|
|
153
|
+
* the key-policy statement when the bucket is KMS-encrypted. Both carry the
|
|
154
|
+
* confused-deputy conditions; the bucket statement additionally requires
|
|
155
|
+
* `bucket-owner-full-control`, the one ACL a BucketOwnerEnforced bucket
|
|
156
|
+
* accepts. Imported buckets and keys are not managed here — a synth
|
|
157
|
+
* warning names the statement the owner must add, because a missing grant
|
|
158
|
+
* fails delivery silently rather than at deploy.
|
|
159
|
+
*/
|
|
160
|
+
grantDeliveryWrite(context, bucket, prefix, deliveryStack) {
|
|
161
|
+
const bucketStack = Stack.of(bucket);
|
|
162
|
+
const principal = new ServicePrincipal(CLOUDFRONT_LOG_DELIVERY_PRINCIPAL);
|
|
163
|
+
const sourceArnPattern = `arn:${bucketStack.partition}:logs:${CLOUDFRONT_LOG_DELIVERY_REGION}:` +
|
|
164
|
+
`${bucketStack.account}:delivery-source:*`;
|
|
165
|
+
// The partition is a token in every stack; the account may be too.
|
|
166
|
+
const sourceArnForHumans = `arn:<partition>:logs:${CLOUDFRONT_LOG_DELIVERY_REGION}:` +
|
|
167
|
+
"<account-id>:delivery-source:*";
|
|
168
|
+
const deputyConditions = {
|
|
169
|
+
StringEquals: { "aws:SourceAccount": bucketStack.account },
|
|
170
|
+
ArnLike: { "aws:SourceArn": sourceArnPattern }
|
|
171
|
+
};
|
|
172
|
+
const bucketGrant = bucket.addToResourcePolicy(new PolicyStatement({
|
|
173
|
+
effect: Effect.ALLOW,
|
|
174
|
+
principals: [principal],
|
|
175
|
+
actions: ["s3:PutObject"],
|
|
176
|
+
resources: [bucket.arnForObjects(`${prefix}/*`)],
|
|
177
|
+
conditions: {
|
|
178
|
+
StringEquals: {
|
|
179
|
+
"s3:x-amz-acl": "bucket-owner-full-control",
|
|
180
|
+
...deputyConditions.StringEquals
|
|
181
|
+
},
|
|
182
|
+
ArnLike: deputyConditions.ArnLike
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
if (!bucketGrant.statementAdded) {
|
|
186
|
+
Annotations.of(this).addWarningV2("fjall:cloudfront:log-delivery-bucket-policy", `${context}: bucket '${bucket.node.path}' is imported, so its policy ` +
|
|
187
|
+
"is not managed here. Allow s3:PutObject on " +
|
|
188
|
+
`'${prefix}/*' to ${CLOUDFRONT_LOG_DELIVERY_PRINCIPAL} with ` +
|
|
189
|
+
"s3:x-amz-acl=bucket-owner-full-control, aws:SourceAccount and " +
|
|
190
|
+
`aws:SourceArn=${sourceArnForHumans} conditions yourself — ` +
|
|
191
|
+
"without it delivery fails silently.");
|
|
192
|
+
}
|
|
193
|
+
else if (bucket.policy !== undefined && bucketStack === deliveryStack) {
|
|
194
|
+
this.delivery.node.addDependency(bucket.policy);
|
|
195
|
+
}
|
|
196
|
+
if (bucket.encryptionKey === undefined)
|
|
197
|
+
return;
|
|
198
|
+
const keyGrant = bucket.encryptionKey.addToResourcePolicy(new PolicyStatement({
|
|
199
|
+
effect: Effect.ALLOW,
|
|
200
|
+
principals: [principal],
|
|
201
|
+
actions: ["kms:GenerateDataKey*", "kms:Decrypt"],
|
|
202
|
+
resources: ["*"],
|
|
203
|
+
conditions: deputyConditions
|
|
204
|
+
}), true);
|
|
205
|
+
if (!keyGrant.statementAdded) {
|
|
206
|
+
Annotations.of(this).addWarningV2("fjall:cloudfront:log-delivery-key-policy", `${context}: the bucket's KMS key '${bucket.encryptionKey.node.path}' ` +
|
|
207
|
+
"is imported, so its key policy is not managed here. Allow " +
|
|
208
|
+
`kms:GenerateDataKey* and kms:Decrypt to ${CLOUDFRONT_LOG_DELIVERY_PRINCIPAL} ` +
|
|
209
|
+
`with aws:SourceAccount and aws:SourceArn=${sourceArnForHumans} ` +
|
|
210
|
+
"conditions yourself — without it delivery fails silently.");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -152,11 +152,14 @@ export default class EcsCluster extends Construct implements IConnectable {
|
|
|
152
152
|
private addServiceToCluster;
|
|
153
153
|
private setupConnections;
|
|
154
154
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
155
|
+
* The outputs the deploy engine's `EcsServiceResolver` reads to find this
|
|
156
|
+
* cluster's services: the cluster's `DeployableCluster` and `ClusterArn`,
|
|
157
|
+
* then one `DeployableService` per service. Export names come from
|
|
158
|
+
* `@fjall/util`'s `stackExportNames` builders — the same functions the
|
|
159
|
+
* resolver rebuilds them with — and the output keys keep the PascalCase
|
|
160
|
+
* form a logical id needs.
|
|
158
161
|
*/
|
|
159
|
-
private
|
|
162
|
+
private addDeployableOutputs;
|
|
160
163
|
/** Checks if any service in the cluster uses a Fargate capacity provider. */
|
|
161
164
|
private anyServiceUsesFargate;
|
|
162
165
|
/** Checks if any service in the cluster uses an EC2 capacity provider. */
|
|
@@ -8,7 +8,7 @@ import { tierDefault } from "../../../utils/backupTierMapping.js";
|
|
|
8
8
|
import { envAwareRemovalPolicyDefault } from "../../../utils/removalPolicy.js";
|
|
9
9
|
import { StackEncryptionKey } from "../secrets/stackEncryptionKey.js";
|
|
10
10
|
import { DEFAULT_FRAMEWORK_LOG_RETENTION, LogGroup } from "../logging/logGroup.js";
|
|
11
|
-
import {
|
|
11
|
+
import { clusterArnExportName, deployableClusterExportName, deployableServiceExportName } from "@fjall/util";
|
|
12
12
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
13
13
|
import { createEcsServiceAlarms, createEcsTaskStopWatchdog, createLogPatternAlarms } from "../monitoring/index.js";
|
|
14
14
|
// Extracted modules
|
|
@@ -177,7 +177,9 @@ export default class EcsCluster extends Construct {
|
|
|
177
177
|
applicationId: props.applicationId
|
|
178
178
|
});
|
|
179
179
|
}
|
|
180
|
-
|
|
180
|
+
if (props.deployableServices !== false) {
|
|
181
|
+
this.addDeployableOutputs(props);
|
|
182
|
+
}
|
|
181
183
|
this.connections = this.setupConnections(props);
|
|
182
184
|
Aspects.of(this).add(new CapacityProviderDependencyAspect(this.cluster));
|
|
183
185
|
}
|
|
@@ -412,16 +414,31 @@ export default class EcsCluster extends Construct {
|
|
|
412
414
|
});
|
|
413
415
|
}
|
|
414
416
|
/**
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
417
|
+
* The outputs the deploy engine's `EcsServiceResolver` reads to find this
|
|
418
|
+
* cluster's services: the cluster's `DeployableCluster` and `ClusterArn`,
|
|
419
|
+
* then one `DeployableService` per service. Export names come from
|
|
420
|
+
* `@fjall/util`'s `stackExportNames` builders — the same functions the
|
|
421
|
+
* resolver rebuilds them with — and the output keys keep the PascalCase
|
|
422
|
+
* form a logical id needs.
|
|
418
423
|
*/
|
|
419
|
-
|
|
424
|
+
addDeployableOutputs(props) {
|
|
425
|
+
const stackName = Stack.of(this).stackName;
|
|
426
|
+
new CfnOutput(this, `${this.outputName}DeployableCluster`, {
|
|
427
|
+
key: `${this.outputName}DeployableCluster`,
|
|
428
|
+
exportName: deployableClusterExportName(stackName, props.clusterName),
|
|
429
|
+
value: this.cluster.clusterArn
|
|
430
|
+
});
|
|
431
|
+
new CfnOutput(this, `${this.outputName}ClusterArn`, {
|
|
432
|
+
key: `${this.outputName}ClusterArn`,
|
|
433
|
+
exportName: clusterArnExportName(stackName, props.clusterName),
|
|
434
|
+
value: this.cluster.clusterArn,
|
|
435
|
+
description: `ECS Cluster ARN for ${props.clusterName}`
|
|
436
|
+
});
|
|
420
437
|
for (const [serviceName, serviceData] of this.services) {
|
|
421
|
-
const
|
|
422
|
-
new CfnOutput(this,
|
|
423
|
-
key:
|
|
424
|
-
exportName:
|
|
438
|
+
const outputKey = `${this.outputName}${toPascalCase(serviceName)}DeployableService`;
|
|
439
|
+
new CfnOutput(this, outputKey, {
|
|
440
|
+
key: outputKey,
|
|
441
|
+
exportName: deployableServiceExportName(stackName, props.clusterName, serviceName),
|
|
425
442
|
value: serviceData.service.serviceArn,
|
|
426
443
|
description: `Deployable ECS Service ARN for ${serviceName} in ${props.clusterName}`
|
|
427
444
|
});
|
|
@@ -467,17 +484,6 @@ export default class EcsCluster extends Construct {
|
|
|
467
484
|
}
|
|
468
485
|
}
|
|
469
486
|
});
|
|
470
|
-
new CfnOutput(this, `${this.outputName}DeployableCluster`, {
|
|
471
|
-
key: `${this.outputName}DeployableCluster`,
|
|
472
|
-
exportName: stackScopedExportName(this, `${props.clusterName}DeployableCluster`),
|
|
473
|
-
value: cluster.clusterArn
|
|
474
|
-
});
|
|
475
|
-
new CfnOutput(this, `${this.outputName}ClusterArn`, {
|
|
476
|
-
key: `${this.outputName}ClusterArn`,
|
|
477
|
-
exportName: stackScopedExportName(this, `${props.clusterName}ClusterArn`),
|
|
478
|
-
value: cluster.clusterArn,
|
|
479
|
-
description: `ECS Cluster ARN for ${props.clusterName}`
|
|
480
|
-
});
|
|
481
487
|
return cluster;
|
|
482
488
|
}
|
|
483
489
|
static build(id, props) {
|
|
@@ -195,6 +195,9 @@ export function getOrCreateAsgCapacityProvider(ctx, serviceProps, state) {
|
|
|
195
195
|
...(ec2Config.persistentDataVolume.alarmTopic === undefined &&
|
|
196
196
|
ctx.props.alertsTopic !== undefined && {
|
|
197
197
|
alarmTopic: ctx.props.alertsTopic
|
|
198
|
+
}),
|
|
199
|
+
...(ctx.props.appBackupTier !== undefined && {
|
|
200
|
+
appBackupTier: ctx.props.appBackupTier
|
|
198
201
|
})
|
|
199
202
|
},
|
|
200
203
|
// Pinned slots reproduce the deployed fjall:OwnerLogicalId verbatim —
|
|
@@ -763,6 +763,17 @@ export interface EcsClusterProps {
|
|
|
763
763
|
* auditable opt-out for cost-sensitive clusters.
|
|
764
764
|
*/
|
|
765
765
|
taskStopWatchdog?: false;
|
|
766
|
+
/**
|
|
767
|
+
* Deployable-service exports (default on): one `DeployableService` output
|
|
768
|
+
* per service plus the cluster's `DeployableCluster` and `ClusterArn`
|
|
769
|
+
* outputs, named by `@fjall/util`'s `stackExportNames` builders. The
|
|
770
|
+
* deploy engine's `EcsServiceResolver` reads the app's Compute stack
|
|
771
|
+
* outputs to find the ECS services that `fjall services`, `fjall rollout`,
|
|
772
|
+
* `fjall restart`, a code-only deploy and a rollback act on. `false` opts
|
|
773
|
+
* out a cluster hosting an infrastructure service, so the resolver never
|
|
774
|
+
* binds it as an app service.
|
|
775
|
+
*/
|
|
776
|
+
deployableServices?: false;
|
|
766
777
|
/**
|
|
767
778
|
* CloudWatch Container Insights. Default: the app backup tier's
|
|
768
779
|
* `containerInsights` column (on at enterprise, off below). Enabling it
|
|
@@ -26,6 +26,14 @@ import type { DomainConfig, EcsCapacityProvider, EcsClusterProps } from "./ecsTy
|
|
|
26
26
|
* a field on `EcsServiceProps`, so a direct `new EcsCluster(...)` consumer
|
|
27
27
|
* cannot pass it and there is no resources-layer code path to validate.
|
|
28
28
|
*
|
|
29
|
+
* Same applies to `cluster.scheduledTasks`: the whole array is a patterns-
|
|
30
|
+
* layer field on `EcsClusterConfig`, materialised into `Ec2TaskDefinition`s
|
|
31
|
+
* and `app.addSchedule(...)` calls by `materialiseScheduledTasks` in
|
|
32
|
+
* `computeEcs.ts`. Its two per-entry refusals (`networkMode: AWS_VPC`, and
|
|
33
|
+
* `logGroup` together with `logRetention`) therefore run in `validateEcsProps`
|
|
34
|
+
* only. `EcsClusterProps` carries no `scheduledTasks` field, so a mirror here
|
|
35
|
+
* would be unreachable.
|
|
36
|
+
*
|
|
29
37
|
* @param props - The cluster props to validate
|
|
30
38
|
* @param annotationsScope - When provided (the `EcsCluster` constructor passes
|
|
31
39
|
* `this`), non-fatal findings from {@link validateEc2TaskMemoryFit} are
|
|
@@ -32,6 +32,14 @@ import { validateSharedEc2CapacityConfig } from "./ecsCapacityConfig.js";
|
|
|
32
32
|
* a field on `EcsServiceProps`, so a direct `new EcsCluster(...)` consumer
|
|
33
33
|
* cannot pass it and there is no resources-layer code path to validate.
|
|
34
34
|
*
|
|
35
|
+
* Same applies to `cluster.scheduledTasks`: the whole array is a patterns-
|
|
36
|
+
* layer field on `EcsClusterConfig`, materialised into `Ec2TaskDefinition`s
|
|
37
|
+
* and `app.addSchedule(...)` calls by `materialiseScheduledTasks` in
|
|
38
|
+
* `computeEcs.ts`. Its two per-entry refusals (`networkMode: AWS_VPC`, and
|
|
39
|
+
* `logGroup` together with `logRetention`) therefore run in `validateEcsProps`
|
|
40
|
+
* only. `EcsClusterProps` carries no `scheduledTasks` field, so a mirror here
|
|
41
|
+
* would be unreachable.
|
|
42
|
+
*
|
|
35
43
|
* @param props - The cluster props to validate
|
|
36
44
|
* @param annotationsScope - When provided (the `EcsCluster` constructor passes
|
|
37
45
|
* `this`), non-fatal findings from {@link validateEc2TaskMemoryFit} are
|
|
@@ -3,6 +3,7 @@ import { Alarm } from "aws-cdk-lib/aws-cloudwatch";
|
|
|
3
3
|
import { EbsDeviceVolumeType, Volume } from "aws-cdk-lib/aws-ec2";
|
|
4
4
|
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
5
5
|
import { Construct } from "constructs";
|
|
6
|
+
import { type BackupTier } from "../../../utils/backupTierMapping.js";
|
|
6
7
|
import { LambdaFunction } from "./lambda.js";
|
|
7
8
|
import { SQSQueue } from "../messaging/sqs.js";
|
|
8
9
|
/**
|
|
@@ -53,6 +54,13 @@ export interface PersistentDataVolumeProps {
|
|
|
53
54
|
* re-attach queue's DLQ-depth alarm (created only when the topic is set).
|
|
54
55
|
*/
|
|
55
56
|
alarmTopic?: ITopic;
|
|
57
|
+
/**
|
|
58
|
+
* The application's backup tier. Enrols the volume in the matching AWS
|
|
59
|
+
* Backup plan via the disaster-recovery tier tag. Independent of the
|
|
60
|
+
* volume's `SNAPSHOT` removal policy, which only covers a clean destroy:
|
|
61
|
+
* the plan's recovery points are the copies that reach the DR region.
|
|
62
|
+
*/
|
|
63
|
+
appBackupTier?: BackupTier;
|
|
56
64
|
}
|
|
57
65
|
/**
|
|
58
66
|
* Standalone EBS data volume + ASG `EC2_INSTANCE_LAUNCHING` hook that
|
|
@@ -10,6 +10,7 @@ import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
|
10
10
|
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
11
11
|
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
12
12
|
import { Construct } from "constructs";
|
|
13
|
+
import { tagBackupTier } from "../../../utils/backupTierMapping.js";
|
|
13
14
|
import { attachInlineAsgLifecycleHook } from "./asgInlineLifecycleHook.js";
|
|
14
15
|
import { LambdaFunction } from "./lambda.js";
|
|
15
16
|
import { SQSQueue } from "../messaging/sqs.js";
|
|
@@ -104,6 +105,7 @@ export class PersistentDataVolume extends Construct {
|
|
|
104
105
|
Tags.of(this.volume).add(PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE, PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE_VALUE);
|
|
105
106
|
Tags.of(this.volume).add(PERSISTENT_DATA_VOLUME_TAG_OWNER_LOGICAL_ID, this.ownerLogicalId);
|
|
106
107
|
Tags.of(this.volume).add(PERSISTENT_DATA_VOLUME_TAG_STACK_ID, Aws.STACK_ID);
|
|
108
|
+
tagBackupTier(this.volume, props.appBackupTier);
|
|
107
109
|
this.queue = new SQSQueue(this, `${id}Queue`, {
|
|
108
110
|
visibilityTimeout: QUEUE_VISIBILITY_TIMEOUT_SECONDS,
|
|
109
111
|
deadLetterQueue: { enabled: true, maxReceiveCount: 5 },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { CfnOutput } from "aws-cdk-lib";
|
|
3
|
-
import { tierDefault } from "../../../utils/backupTierMapping.js";
|
|
3
|
+
import { tagBackupTier, tierDefault } from "../../../utils/backupTierMapping.js";
|
|
4
4
|
import { toRemovalPolicy } from "../../../utils/removalPolicy.js";
|
|
5
5
|
import { StackEncryptionKey } from "../secrets/stackEncryptionKey.js";
|
|
6
6
|
import { Table, AttributeType, BillingMode, StreamViewType, ProjectionType, TableEncryption } from "aws-cdk-lib/aws-dynamodb";
|
|
@@ -98,6 +98,7 @@ export class DynamoDBTable extends Construct {
|
|
|
98
98
|
removalPolicy: toRemovalPolicy(props.removalPolicy),
|
|
99
99
|
timeToLiveAttribute: props.timeToLiveAttribute
|
|
100
100
|
});
|
|
101
|
+
tagBackupTier(this.table, props.appBackupTier);
|
|
101
102
|
if (props.globalSecondaryIndexes) {
|
|
102
103
|
for (const gsi of props.globalSecondaryIndexes) {
|
|
103
104
|
const gsiProps = {
|
|
@@ -6,7 +6,7 @@ import { Construct } from "constructs";
|
|
|
6
6
|
import { SecurityGroup } from "../networking/securityGroup.js";
|
|
7
7
|
import { Secret } from "../secrets/index.js";
|
|
8
8
|
import { ResourceNaming } from "../../../utils/resourceNaming.js";
|
|
9
|
-
import { tierDefault } from "../../../utils/backupTierMapping.js";
|
|
9
|
+
import { tagBackupTier, tierDefault } from "../../../utils/backupTierMapping.js";
|
|
10
10
|
import { addProxyCfnOutput } from "./rdsProxyOutput.js";
|
|
11
11
|
import { getCdkDatabaseInsightsMode, getDatabaseInsightsRetention } from "./index.js";
|
|
12
12
|
import { RDS_DEFAULTS, SERVERLESS_V2_AUTO_PAUSE_MIN_SECONDS, SERVERLESS_V2_AUTO_PAUSE_MAX_SECONDS, MIN_AUTO_PAUSE_POSTGRES_VERSION } from "./rdsDefaults.js";
|
|
@@ -255,6 +255,11 @@ export class RdsAurora extends Construct {
|
|
|
255
255
|
this.databaseCluster = new DatabaseCluster(this, `${id}Database`, clusterProps);
|
|
256
256
|
}
|
|
257
257
|
this.cfnCluster = this.databaseCluster.node.defaultChild;
|
|
258
|
+
// The L1, not the L2: AWS Backup protects the Aurora cluster, and a tag on
|
|
259
|
+
// the DatabaseCluster construct would propagate to every member instance,
|
|
260
|
+
// each of which would then be selected as an RDS instance it cannot back
|
|
261
|
+
// up.
|
|
262
|
+
tagBackupTier(this.cfnCluster, props.appBackupTier);
|
|
258
263
|
// Global secondary clusters can't specify these properties
|
|
259
264
|
if (props.isGlobalSecondary && this.cfnCluster) {
|
|
260
265
|
this.cfnCluster.masterUsername = undefined;
|
|
@@ -13,7 +13,7 @@ import { RDS_DEFAULTS } from "./rdsDefaults.js";
|
|
|
13
13
|
import { DEFAULT_POSTGRES_ENGINE_CONFIG, resolveDatabaseInsights, resolveStorageEncryptionKey, resolvePerformanceInsightsKey, addMultiUserSecretRotation, warnIfSnapshotUsernameAssumed } from "./rdsHelpers.js";
|
|
14
14
|
import { resolveSecretRotation, isCMKRequested } from "../../../utils/databaseTypes.js";
|
|
15
15
|
import { ResourceNaming } from "../../../utils/resourceNaming.js";
|
|
16
|
-
import { tierDefault } from "../../../utils/backupTierMapping.js";
|
|
16
|
+
import { tagBackupTier, tierDefault } from "../../../utils/backupTierMapping.js";
|
|
17
17
|
import { addProxyCfnOutput } from "./rdsProxyOutput.js";
|
|
18
18
|
import { createRdsAlarms } from "../monitoring/index.js";
|
|
19
19
|
export class RdsInstance extends Construct {
|
|
@@ -189,6 +189,11 @@ export class RdsInstance extends Construct {
|
|
|
189
189
|
credentials: Credentials.fromSecret(this.databaseCredentials.secret)
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
|
+
// The L1, not the L2: a tag on the DatabaseInstance construct propagates
|
|
193
|
+
// to the subnet group it creates. The read replica added below is
|
|
194
|
+
// deliberately left untagged — AWS Backup would protect it as a second
|
|
195
|
+
// full copy of storage the primary's own recovery points already hold.
|
|
196
|
+
tagBackupTier(this.database.node.defaultChild, props.appBackupTier);
|
|
192
197
|
}
|
|
193
198
|
rotateSecret(rotationConfig) {
|
|
194
199
|
this.masterSecret = addMultiUserSecretRotation({
|
|
@@ -9,16 +9,23 @@ export declare const ACCESS_LOG_RETENTION_DAYS = 90;
|
|
|
9
9
|
/**
|
|
10
10
|
* The one bucket an enterprise-tier stack delivers its access logs to —
|
|
11
11
|
* application load balancer access logs (`alb/<name>/`) and CloudFront
|
|
12
|
-
* standard logs (`cloudfront/<id>/`) share it under distinct prefixes.
|
|
12
|
+
* standard logs v2 (`cloudfront/<id>/`) share it under distinct prefixes.
|
|
13
13
|
*
|
|
14
14
|
* Its shape is dictated by the two delivery services and deliberately NOT
|
|
15
15
|
* tier-aware: ELB log delivery requires SSE-S3 (it cannot write to a
|
|
16
|
-
* KMS-encrypted bucket)
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `mitigated` rather than
|
|
21
|
-
*
|
|
22
|
-
* non-production DESTROYs.
|
|
16
|
+
* KMS-encrypted bucket); both deliveries are bucket-policy grants, so
|
|
17
|
+
* ownership is `BUCKET_OWNER_ENFORCED` (ACLs off — the S3 default, and the
|
|
18
|
+
* posture every other Fjall bucket carries); and versioning would only
|
|
19
|
+
* multiply expired log objects — the `access-logs` exemption marker tells
|
|
20
|
+
* posture so, landing `S3_VERSIONING_DISABLED` `mitigated` rather than
|
|
21
|
+
* `open`. The bucket takes the env-aware removal policy like every other
|
|
22
|
+
* bucket: production RETAINs its audit evidence, non-production DESTROYs.
|
|
23
|
+
*
|
|
24
|
+
* Migration from 33.0.0–37.x: CloudFront standard (legacy) logging granted
|
|
25
|
+
* its `awslogsdelivery` canonical user FULL_CONTROL on the bucket ACL and
|
|
26
|
+
* never removes it, so on a bucket legacy logging wrote to the ownership
|
|
27
|
+
* flip fails with `InvalidBucketAclWithObjectOwnership`. Cure once, then
|
|
28
|
+
* redeploy: `aws s3api put-bucket-acl --bucket <name> --acl private`. The
|
|
29
|
+
* objects already delivered stay readable under the old prefix.
|
|
23
30
|
*/
|
|
24
31
|
export declare function stackAccessLogBucket(scope: Construct): S3Bucket;
|