in packages/secure-static-site/lib/static-site.ts [125:304]
constructor(scope: Construct, id: string, props: StaticSiteProps) {
super(scope, id);
const {
path = "./",
distFolder = "dist",
envVars,
buildCommand = "npm run build",
allowedIPs,
responseHeaders,
enableWaf,
disableCoreWafRuleGroup,
disableAmazonIPWafRuleGroup,
disableAnonymousIPWafRuleGroup,
domainNameBase,
domainNamePrefix,
} = props;
const enableWafMetrics = !!props.enableWafMetrics;
// S3
const serverAccessLogsBucket = new Bucket(this, "ServerAccessLogsBucket", {
autoDeleteObjects: true,
removalPolicy: RemovalPolicy.DESTROY,
enforceSSL: true,
encryption: BucketEncryption.S3_MANAGED,
lifecycleRules: [
{
transitions: [
{
storageClass: StorageClass.INTELLIGENT_TIERING,
transitionAfter: Duration.days(0),
}
]
}
]
});
this.bucket = new Bucket(this, "StaticSiteBucket", {
autoDeleteObjects: true,
removalPolicy: RemovalPolicy.DESTROY,
enforceSSL: true,
encryption: BucketEncryption.S3_MANAGED,
serverAccessLogsBucket,
serverAccessLogsPrefix: "s3"
});
// WAF
let webACL: CfnWebACL | undefined = undefined;
if (enableWaf) {
// allow requests that are not blocked by other rules by default
let defaultAction: CfnWebACL.RuleActionProperty = { allow: {} };
let ipRuleSet: CfnIPSet | undefined = undefined;
if (allowedIPs) {
defaultAction = { block: {} };
ipRuleSet = new CfnIPSet(this, "IPRuleSet", {
addresses: allowedIPs,
ipAddressVersion: "IPV4",
scope: "CLOUDFRONT",
});
}
// create WAF rules using relevant props
const rules: CfnWebACL.RuleProperty[] = createWafRules({
enableWafMetrics,
disableAmazonIPWafRuleGroup,
disableAnonymousIPWafRuleGroup,
disableCoreWafRuleGroup,
allowedIPs,
ipRuleSet,
});
// For CLOUDFRONT, you must create your WAFv2 resources in the US East (N. Virginia) Region, us-east-1.
webACL = new CfnWebACL(this, "WebACL", {
defaultAction,
rules,
scope: "CLOUDFRONT",
visibilityConfig: {
cloudWatchMetricsEnabled: enableWafMetrics,
metricName: "DefaultMetric",
sampledRequestsEnabled: enableWafMetrics,
},
});
}
// CloudFront
let distributionProps: DistributionProps = {
defaultBehavior: {
origin: new S3Origin(this.bucket),
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
functionAssociations: [
{
function: new Function(this, "SecurityHeadersFn", {
code: FunctionCode.fromInline(getFunctionCode(responseHeaders)),
// explicit function name needed b/c https://github.com/aws/aws-cdk/issues/15523
functionName: `SecureStaticSite${this.node.addr}`,
}),
eventType: FunctionEventType.VIEWER_RESPONSE,
},
],
},
defaultRootObject: "index.html",
errorResponses: [
{
httpStatus: 404,
responseHttpStatus: 200,
responsePagePath: "/index.html",
},
],
enableLogging: true,
logBucket: serverAccessLogsBucket,
logFilePrefix: "cloudfront",
minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021,
};
// add WAF web ACL to distribution if present
if (webACL) {
distributionProps = { ...distributionProps, webAclId: webACL.attrArn };
}
// configure Route 53 only if domain name base and prefix are set
if (domainNameBase && domainNamePrefix) {
this.zone = HostedZone.fromLookup(this, "StaticSiteHostedZone", {
domainName: domainNameBase,
});
// prefix allows multiple apps to use same base
this.fullDomainName = `${domainNamePrefix}.${domainNameBase}`;
// can only create certificate in CDK if using Route 53 for DNS
const certificate = new Certificate(this, "StaticSiteCertificate", {
domainName: this.fullDomainName,
validation: CertificateValidation.fromDns(this.zone),
// allow subdomains (e.g. www, test, stage, etc)
subjectAlternativeNames: [`*.${this.fullDomainName}`],
});
// update CloudFront distribution with domain names and certificate
distributionProps = {
...distributionProps,
domainNames: [this.fullDomainName, `www.${this.fullDomainName}`],
certificate,
};
}
this.distribution = new Distribution(
this,
"StaticSiteDistribution",
distributionProps
);
// hosted zone and full domain name must exist to create Route 53 records
if (this.zone && this.fullDomainName) {
// IPV4
new ARecord(this, "StaticSiteARecord", {
zone: this.zone,
recordName: this.fullDomainName,
target: RecordTarget.fromAlias(new CloudFrontTarget(this.distribution)),
});
new ARecord(this, "StaticSiteSubsiteARecord", {
zone: this.zone,
recordName: `*.${this.fullDomainName}`,
target: RecordTarget.fromAlias(new CloudFrontTarget(this.distribution)),
});
// IPV6
new AaaaRecord(this, "StaticSiteAaaaRecord", {
zone: this.zone,
recordName: this.fullDomainName,
target: RecordTarget.fromAlias(new CloudFrontTarget(this.distribution)),
});
new AaaaRecord(this, "StaticSiteSubsiteAaaaRecord", {
zone: this.zone,
recordName: `*.${this.fullDomainName}`,
target: RecordTarget.fromAlias(new CloudFrontTarget(this.distribution)),
});
}
// TODO: create CloudFront Function to specify Content-Security-Policy
buildStaticSite({ path, buildCommand, envVars });
new BucketDeployment(this, "BucketDeployment", {
destinationBucket: this.bucket,
sources: [Source.asset(resolve(path, distFolder))],
distribution: this.distribution,
});
}