-
Notifications
You must be signed in to change notification settings - Fork 0
/
stack.ts
207 lines (190 loc) · 6.58 KB
/
stack.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
import { Construct } from 'constructs';
import {
CfnOutput,
Duration,
RemovalPolicy,
StackProps,
aws_certificatemanager as acm,
aws_cloudfront as cloudfront,
aws_cloudfront_origins as cloudfrontOrigins,
aws_s3 as s3,
aws_s3_deployment as s3deploy,
} from 'aws-cdk-lib';
export interface FrontendConstructProps extends StackProps {
/**
* The domain name for the site to use
*/
readonly domainNames?: string[];
/**
* Location of FE code to deploy
*/
readonly deploymentSource: string;
/**
* Optional custom sources for CloudFront to proxy to
*/
readonly customBehaviors?: Record<string, cloudfront.BehaviorOptions>;
/**
* Optional additional paths to files that should not be cached
* Includes index.html, robots.txt, favicon.ico, and config.json by default
*/
readonly noCachePaths?: string[];
/**
* Override the CloudFront distribution ID for migration purposes
*/
readonly distributionLocalIdOverride?: string;
}
// some code taken from https://github.com/aws-samples/aws-cdk-examples/blob/master/typescript/static-site/static-site.ts
export class FrontendConstruct extends Construct {
private readonly noCachePaths: string[];
private readonly certificate: acm.Certificate;
public readonly distribution: cloudfront.Distribution;
constructor(parent: Construct, id: string, props: FrontendConstructProps) {
super(parent, id);
this.noCachePaths = [
...(props.noCachePaths ? props.noCachePaths : []),
'index.html',
'robots.txt',
'favicon.ico',
'config.json',
'ngsw.json',
];
// Content bucket
const siteBucket = new s3.Bucket(this, 'SiteBucket', {
removalPolicy: RemovalPolicy.DESTROY,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
if (props.domainNames) {
// TLS certificate
this.certificate = new acm.Certificate(this, 'SiteCertificate', {
domainName: props.domainNames[0],
subjectAlternativeNames: props.domainNames.slice(1),
validation: acm.CertificateValidation.fromDns(),
});
new CfnOutput(this, 'Certificate', {
value: this.certificate.certificateArn,
});
}
const origin = new cloudfrontOrigins.S3Origin(siteBucket);
const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(
this,
'CloudFrontResponseHeaders',
{
securityHeadersBehavior: {
strictTransportSecurity: {
accessControlMaxAge: Duration.days(365 * 2),
includeSubdomains: true,
preload: true,
override: true,
},
contentTypeOptions: {
override: true,
},
xssProtection: {
modeBlock: true,
override: true,
protection: true,
},
frameOptions: {
frameOption: cloudfront.HeadersFrameOption.SAMEORIGIN,
override: true,
},
referrerPolicy: {
referrerPolicy: cloudfront.HeadersReferrerPolicy.STRICT_ORIGIN,
override: true,
},
},
}
);
const defaultBehavior: cloudfront.BehaviorOptions = {
origin,
compress: true,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
responseHeadersPolicy,
};
const noCacheBehavior: cloudfront.BehaviorOptions = {
...defaultBehavior,
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
};
this.distribution = new cloudfront.Distribution(this, 'SiteDistribution', {
domainNames: props.domainNames ? props.domainNames : undefined,
certificate: props.domainNames ? this.certificate : undefined,
errorResponses: [
{
httpStatus: 403,
responseHttpStatus: 200,
responsePagePath: '/index.html',
},
{
httpStatus: 404,
responseHttpStatus: 200,
responsePagePath: '/index.html',
},
],
defaultRootObject: 'index.html',
defaultBehavior,
priceClass: cloudfront.PriceClass.PRICE_CLASS_100,
enableIpv6: true,
httpVersion: cloudfront.HttpVersion.HTTP2,
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
additionalBehaviors: {
...this.noCachePaths.reduce((obj, path) => {
obj[path] = noCacheBehavior;
return obj;
}, {} as Record<string, any>),
...(props.customBehaviors ? props.customBehaviors : {}),
},
});
// used for migration from CloudFrontWebDistribution to Distribution
// https://stackoverflow.com/a/68764093/5991792
if (props.distributionLocalIdOverride) {
(
this.distribution.node.defaultChild as cloudfront.CfnDistribution
).overrideLogicalId(props.distributionLocalIdOverride);
}
new CfnOutput(this, 'DistributionId', {
value: this.distribution.distributionId,
});
new CfnOutput(this, 'DistributionDomainname', {
value: this.distribution.distributionDomainName,
});
const s3Asset = s3deploy.Source.asset(props.deploymentSource);
// https://blog.kewah.com/2021/cdk-pattern-static-files-s3-cloudfront/
const deployment = new s3deploy.BucketDeployment(this, 'S3Deployment', {
sources: [s3Asset],
destinationBucket: siteBucket,
retainOnDelete: true,
// remove from this deployment since we do it below, so as to not run two invalidations at once
// distribution: this.distribution,
memoryLimit: 1769, // one full vCPU
exclude: this.noCachePaths,
cacheControl: [
s3deploy.CacheControl.setPublic(),
s3deploy.CacheControl.maxAge(Duration.days(365)),
s3deploy.CacheControl.fromString('immutable'),
],
});
const noCacheDeployment = new s3deploy.BucketDeployment(
this,
'S3DeploymentNoCache',
{
sources: [s3Asset],
destinationBucket: siteBucket,
retainOnDelete: true,
distribution: this.distribution,
memoryLimit: 1769, // one full vCPU
exclude: ['*'],
include: this.noCachePaths,
cacheControl: [
s3deploy.CacheControl.setPublic(),
s3deploy.CacheControl.maxAge(Duration.days(0)),
s3deploy.CacheControl.sMaxAge(Duration.days(0)),
s3deploy.CacheControl.fromString('must-revalidate'),
s3deploy.CacheControl.fromString('proxy-revalidate'),
s3deploy.CacheControl.fromString('no-store'),
],
}
);
noCacheDeployment.node.addDependency(deployment); // ensure deployment goes before noCacheDeployment so that bucket is pruned first
}
}